@joshuafolkken/kit 1.1166.0 → 1.1229.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 (437) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/settings.json +2 -2
  3. package/.claude/skills/dependency-update/SKILL.md +4 -8
  4. package/.claude/skills/epic-commands/SKILL.md +2 -2
  5. package/.claude/skills/epic-commands/epic-bundle.md +1 -1
  6. package/.claude/skills/workflow-commands/SKILL.md +2 -4
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +9 -5
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +29 -9
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -2
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +24 -15
  13. package/.claude/skills/workflow-commands/chain-rule.md +1 -1
  14. package/.claude/skills/workflow-commands/delegation.md +2 -2
  15. package/.claude/skills/workflow-commands/followup.md +5 -0
  16. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  18. package/.claude/skills/workflow-commands/issue-scout.md +1 -1
  19. package/.claude/skills/workflow-commands/kickoff.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +10 -5
  22. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  23. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  25. package/.codex/hooks.json +2 -2
  26. package/.cursorrules +6 -41
  27. package/.github/actions/setup-node/action.yml +1 -1
  28. package/.github/workflows/auto-tag.yml +3 -4
  29. package/.github/workflows/ci.yml +47 -33
  30. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  31. package/.github/workflows/publish.yml +48 -4
  32. package/.github/workflows/sonar-qube.yml +3 -0
  33. package/.pnpmfile.mjs +4 -3
  34. package/.prettierignore +8 -2
  35. package/CLAUDE.md +18 -20
  36. package/README.md +26 -19
  37. package/SECURITY.md +15 -1
  38. package/dist/CLAUDE.md +18 -20
  39. package/dist/commands/document-section-cli.js +734 -0
  40. package/dist/hooks/audit-provision.js +346 -0
  41. package/dist/hooks/batch-guard.js +5 -4
  42. package/dist/hooks/{chunk-DYAVAM6S.js → chunk-55HHG5PX.js} +1405 -631
  43. package/dist/hooks/{chunk-A6P7PN7P.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-5KTCMCOT.js → chunk-6HJJXRBX.js} +4 -2
  46. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  47. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  48. package/dist/hooks/{chunk-ENA7P5FB.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-ANJ2WGFD.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-GU5MATVE.js → chunk-JUDKDCDP.js} +499 -389
  52. package/dist/hooks/{chunk-ZDZ555YH.js → chunk-NV2JRZZO.js} +743 -909
  53. package/dist/hooks/{chunk-V2BTOZHT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-URJA6XY4.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-GYNGP7HI.js → chunk-VC3PZPZO.js} +1 -1
  56. package/dist/hooks/codex-hook-adapter.js +14 -12
  57. package/dist/hooks/duplicate-read-guard.js +6 -5
  58. package/dist/hooks/format-edited.js +4 -3
  59. package/dist/hooks/inputs.json +232 -210
  60. package/dist/hooks/investigation-guard.js +6 -5
  61. package/dist/hooks/pretool-guard.js +12 -10
  62. package/dist/hooks/session-lang.js +2 -4
  63. package/dist/hooks/stop-guard.js +81 -59
  64. package/dist/josh.js +872 -75
  65. package/dist/version/index.d.ts +7 -2
  66. package/dist/version/index.js +152 -75
  67. package/eslint/base.js +23 -1
  68. package/eslint/rules/code-quality.js +3 -0
  69. package/eslint/rules/early-return-one-liner.js +98 -0
  70. package/eslint/rules/unicorn.js +16 -1
  71. package/package.json +7 -6
  72. package/pnpm-workspace.yaml +9 -4
  73. package/prettier/basic.d.ts +3 -1
  74. package/prettier/index.d.ts +3 -1
  75. package/prettier/static.d.ts +3 -1
  76. package/prompts/coding-standards.md +1 -1
  77. package/prompts/collaboration-workflow/file-edits.md +4 -4
  78. package/prompts/collaboration-workflow/glossary.md +1 -1
  79. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  80. package/prompts/collaboration-workflow/overview.md +2 -2
  81. package/prompts/collaboration-workflow/principles.md +14 -12
  82. package/prompts/collaboration-workflow/report-format.md +1 -1
  83. package/prompts/collaboration-workflow/residency.md +1 -1
  84. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  85. package/prompts/collaboration-workflow/shell-body.md +6 -6
  86. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  87. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  88. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  89. package/prompts/review-rubric.md +1 -2
  90. package/prompts/review.md +1 -1
  91. package/scripts/agent/agent-headless.ts +29 -0
  92. package/scripts/agent/agent-launch-environment.ts +6 -2
  93. package/scripts/agent/codex-agent-argv.ts +2 -1
  94. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  95. package/scripts/backlog/backlog-arrival.ts +3 -3
  96. package/scripts/backlog/backlog-budget.ts +1 -1
  97. package/scripts/backlog/backlog-drive-args.ts +135 -0
  98. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  99. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  100. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  101. package/scripts/backlog/backlog-drive-named.ts +3 -3
  102. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  103. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  104. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  105. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  106. package/scripts/backlog/backlog-drive.ts +1 -1
  107. package/scripts/backlog/backlog-next.ts +31 -9
  108. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  109. package/scripts/backlog/backlog-offer.ts +1 -1
  110. package/scripts/backlog/backlog-plan-cli.ts +7 -6
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-rank.ts +18 -9
  113. package/scripts/backlog/backlog-ready.ts +1 -1
  114. package/scripts/backlog/backlog-restructure.ts +152 -0
  115. package/scripts/backlog/backlog-scope.ts +1 -1
  116. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  117. package/scripts/backlog/backlog-stalled.ts +1 -1
  118. package/scripts/backlog/backlog-waves.ts +15 -17
  119. package/scripts/build/build-codex-hooks.ts +3 -1
  120. package/scripts/build/build-commands.ts +66 -0
  121. package/scripts/build/build-hooks.ts +2 -1
  122. package/scripts/build/build-library.ts +12 -3
  123. package/scripts/build/build.ts +70 -0
  124. package/scripts/build/local-bin.ts +3 -13
  125. package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
  126. package/scripts/cost-runtime/cost-verdict.ts +15 -28
  127. package/scripts/delegation/investigation-reads.ts +3 -1
  128. package/scripts/discovery/repo-origin.ts +5 -5
  129. package/scripts/doctor/doctor-consumer.ts +2 -2
  130. package/scripts/doctor/doctor-io.ts +2 -3
  131. package/scripts/document/capped-print-part.ts +30 -0
  132. package/scripts/document/capped-print.ts +3 -4
  133. package/scripts/document/document-byte-budget.ts +11 -2
  134. package/scripts/document/document-scan.ts +117 -0
  135. package/scripts/document/document-section.ts +143 -16
  136. package/scripts/document/entry-read-budget.ts +5 -3
  137. package/scripts/document/entry-read-set.ts +10 -89
  138. package/scripts/document/read-set-cost.ts +98 -0
  139. package/scripts/epic/epic-add-body.ts +1 -0
  140. package/scripts/epic/epic-audit-checks.ts +1 -1
  141. package/scripts/epic/epic-audit-rationale.ts +4 -3
  142. package/scripts/epic/epic-bundle-cli.ts +14 -3
  143. package/scripts/epic/epic-bundle-poll.ts +41 -0
  144. package/scripts/epic/epic-busy.ts +1 -1
  145. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  146. package/scripts/epic/epic-classify.ts +1 -1
  147. package/scripts/epic/epic-cli-argv.ts +78 -46
  148. package/scripts/epic/epic-cli-remove.ts +11 -19
  149. package/scripts/epic/epic-cli.ts +74 -73
  150. package/scripts/epic/epic-close-comment.ts +1 -1
  151. package/scripts/epic/epic-close.ts +1 -1
  152. package/scripts/epic/epic-cross-repo.ts +5 -5
  153. package/scripts/epic/epic-graph.ts +1 -1
  154. package/scripts/epic/epic-index.ts +1 -1
  155. package/scripts/epic/epic-issue.ts +2 -6
  156. package/scripts/epic/epic-lane-offer.ts +2 -2
  157. package/scripts/epic/epic-next.ts +29 -42
  158. package/scripts/epic/epic-solo-stale.ts +1 -1
  159. package/scripts/epic/epic-triage.ts +8 -4
  160. package/scripts/epic/epic-validate.ts +1 -1
  161. package/scripts/followup/git-followup-finish.ts +2 -2
  162. package/scripts/followup/git-followup-label.ts +2 -2
  163. package/scripts/followup/git-followup-merged.ts +2 -2
  164. package/scripts/followup/git-followup-stages.ts +9 -0
  165. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  166. package/scripts/followup/git-pr-followup.ts +5 -130
  167. package/scripts/gate/core-budget.ts +69 -1
  168. package/scripts/gate/gate-plan.ts +47 -7
  169. package/scripts/gate/gate-skip.ts +38 -7
  170. package/scripts/gate/hook-gate-reuse.ts +12 -10
  171. package/scripts/gate/pre-commit-type-check.ts +4 -1
  172. package/scripts/gate/project-checks.ts +6 -9
  173. package/scripts/gate/type-check-step.ts +2 -3
  174. package/scripts/gate/verification-gate.ts +27 -8
  175. package/scripts/gh/git-gh-api-path.ts +2 -2
  176. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  177. package/scripts/gh/git-gh-issue-write.ts +53 -7
  178. package/scripts/gh/git-gh-issue.ts +5 -3
  179. package/scripts/gh/git-gh-pr-read.ts +68 -19
  180. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  181. package/scripts/gh/git-gh-pr.ts +5 -1
  182. package/scripts/gh/git-pr-ai-review.ts +1 -1
  183. package/scripts/gh/git-pr-checks-eval.ts +9 -3
  184. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  185. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  186. package/scripts/gh/git-pr-checks.ts +18 -6
  187. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  188. package/scripts/gh/git-pr-error.ts +1 -3
  189. package/scripts/gh/git-pr.ts +9 -13
  190. package/scripts/gh/github-issue-url.ts +2 -2
  191. package/scripts/git/constants.ts +12 -6
  192. package/scripts/git/git-command.ts +20 -218
  193. package/scripts/git/git-diff-reads.ts +225 -0
  194. package/scripts/git/git-error.ts +4 -12
  195. package/scripts/git/git-pre-push-hook.ts +139 -0
  196. package/scripts/git/git-prompt.ts +2 -6
  197. package/scripts/git/{schemas.ts → git-schemas.ts} +2 -0
  198. package/scripts/git/git-spawn-sync.ts +3 -1
  199. package/scripts/git/git-spawn.ts +1 -1
  200. package/scripts/git/git-staging.ts +4 -6
  201. package/scripts/git/git-status.ts +3 -9
  202. package/scripts/git/main-merge.ts +57 -21
  203. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  204. package/scripts/hooks/edited-cspell.ts +2 -4
  205. package/scripts/hooks/format-edited-file.ts +3 -12
  206. package/scripts/hooks/pre-push-unit.ts +31 -9
  207. package/scripts/hooks/reserved-run.ts +2 -2
  208. package/scripts/hooks/run-hook.sh +5 -4
  209. package/scripts/hooks/stop-guard.ts +5 -3
  210. package/scripts/init/init-actions.ts +0 -5
  211. package/scripts/init/init-ai-copy.ts +13 -2
  212. package/scripts/init/init-logic-json-merge.ts +19 -0
  213. package/scripts/init/init-logic-secretlint.ts +7 -7
  214. package/scripts/init/init-logic-workspace.ts +41 -2
  215. package/scripts/init/init-logic.ts +16 -26
  216. package/scripts/init/init.ts +5 -15
  217. package/scripts/init/initial-commit.ts +4 -4
  218. package/scripts/init/kit-development-versions.ts +40 -0
  219. package/scripts/init/kit-setup-state.ts +2 -2
  220. package/scripts/init/start-exec.ts +68 -19
  221. package/scripts/init/start-setup-pr.ts +9 -9
  222. package/scripts/init/start-steps.ts +6 -10
  223. package/scripts/issue/defect-rate-cli.ts +1 -1
  224. package/scripts/issue/git-next-issues.ts +1 -1
  225. package/scripts/issue/issue-auto-ok.ts +1 -1
  226. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  227. package/scripts/issue/issue-backlinks.ts +9 -9
  228. package/scripts/issue/issue-comment-cli.ts +19 -13
  229. package/scripts/issue/issue-file-cli.ts +45 -18
  230. package/scripts/issue/issue-file.ts +24 -1
  231. package/scripts/issue/issue-scout-closed.ts +1 -1
  232. package/scripts/issue/issue-state-cli.ts +30 -74
  233. package/scripts/josh/josh-command-map.ts +2 -0
  234. package/scripts/josh/josh-command-types.ts +11 -0
  235. package/scripts/josh/josh-commands-ai.ts +4 -4
  236. package/scripts/josh/josh-commands-backlog.ts +4 -1
  237. package/scripts/josh/josh-commands-development.ts +11 -1
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-lane.ts +22 -0
  240. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  241. package/scripts/josh/josh-commands-run.ts +12 -7
  242. package/scripts/josh/josh-in-process.ts +28 -9
  243. package/scripts/josh/josh-logic.ts +33 -15
  244. package/scripts/josh/process-identity.ts +1 -3
  245. package/scripts/josh/session-language-cli.ts +3 -4
  246. package/scripts/lane/lane-await.ts +5 -1
  247. package/scripts/lane/lane-child-invocation.ts +2 -2
  248. package/scripts/lane/lane-cli.ts +3 -3
  249. package/scripts/lane/lane-dispatch.ts +14 -10
  250. package/scripts/lane/lane-environment.ts +5 -1
  251. package/scripts/lane/lane-handoff.ts +1 -1
  252. package/scripts/lane/lane-install.ts +2 -13
  253. package/scripts/lane/lane-launch-cli.ts +35 -3
  254. package/scripts/lane/lane-ledger.ts +145 -0
  255. package/scripts/lane/lane-leftover.ts +1 -1
  256. package/scripts/lane/lane-load.ts +108 -0
  257. package/scripts/lane/lane-measure-cli.ts +149 -0
  258. package/scripts/lane/lane-occupancy.ts +3 -0
  259. package/scripts/lane/lane-reap.ts +1 -3
  260. package/scripts/lane/lane-relaunch.ts +1 -1
  261. package/scripts/lane/lane-stats.ts +224 -0
  262. package/scripts/lane/lane-vacant.ts +36 -0
  263. package/scripts/lane/openai-lane-ship.ts +2 -2
  264. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  265. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  266. package/scripts/lane/openai-review-broker.ts +2 -4
  267. package/scripts/lib/ancestor-directories.ts +35 -0
  268. package/scripts/lib/animation-helpers.ts +2 -2
  269. package/scripts/lib/buffered-process.ts +3 -7
  270. package/scripts/lib/cli-flags.ts +71 -1
  271. package/scripts/lib/spawn-exit.ts +2 -2
  272. package/scripts/lib/timeouts.ts +35 -2
  273. package/scripts/lines/line-budget.ts +4 -6
  274. package/scripts/notify/telegram-notify.ts +26 -1
  275. package/scripts/notify/telegram-test.ts +6 -0
  276. package/scripts/observations/observation-ledger-line.ts +6 -6
  277. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  278. package/scripts/observations/observation-ledger.ts +26 -7
  279. package/scripts/overrides/lockfile-overrides.ts +1 -1
  280. package/scripts/overrides/overrides-logic.ts +1 -1
  281. package/scripts/propagate/propagate-git.ts +1 -1
  282. package/scripts/propagate/propagate-publish.ts +2 -2
  283. package/scripts/propagate/propagate-steps.ts +20 -15
  284. package/scripts/propagate/propagate.ts +36 -27
  285. package/scripts/refactor/import-graph.ts +18 -1
  286. package/scripts/refactor/refactor-lint.ts +4 -4
  287. package/scripts/repo/repository-labels.ts +2 -2
  288. package/scripts/repo/required-checks-logic.ts +1 -3
  289. package/scripts/review/live-evidence.ts +2 -4
  290. package/scripts/review/review-brief-cli.ts +1 -1
  291. package/scripts/review/review-finding-ledger.ts +1 -1
  292. package/scripts/review/review-findings-cli.ts +1 -3
  293. package/scripts/rules/decision-oracle-batch.ts +128 -0
  294. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  295. package/scripts/rules/decision-oracle.ts +8 -120
  296. package/scripts/rules/delivered-rules.ts +26 -182
  297. package/scripts/rules/direct-filing.ts +1 -1
  298. package/scripts/rules/early-heartbeat.ts +3 -3
  299. package/scripts/rules/git-argv.ts +1 -1
  300. package/scripts/rules/git-force.ts +1 -1
  301. package/scripts/rules/implementation-cut.ts +2 -2
  302. package/scripts/rules/index-guard.ts +1 -1
  303. package/scripts/rules/issue-comments.ts +118 -0
  304. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  305. package/scripts/rules/lane-background.ts +2 -2
  306. package/scripts/rules/lane-park.ts +18 -0
  307. package/scripts/rules/lane-split-park.ts +128 -0
  308. package/scripts/rules/oracle-firing.ts +1 -1
  309. package/scripts/rules/poll-loop.ts +1 -1
  310. package/scripts/rules/pre-gate-cut.ts +3 -3
  311. package/scripts/rules/protected-files.ts +59 -4
  312. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  313. package/scripts/rules/shell-body-trigger.ts +1 -1
  314. package/scripts/rules/stop-rules.ts +18 -7
  315. package/scripts/rules/test-declared-commit.ts +4 -4
  316. package/scripts/rules/wip-cap.ts +63 -0
  317. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  318. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  319. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  320. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +12 -6
  321. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  322. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +3 -3
  323. package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +2 -2
  324. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  325. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  326. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  327. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  328. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  329. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  330. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  331. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  332. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  333. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  334. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  335. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  336. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  337. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  338. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  339. package/scripts/run/run-ending.ts +2 -2
  340. package/scripts/run/run-halfrun-resume.ts +6 -1
  341. package/scripts/run/run-headless.ts +1 -1
  342. package/scripts/run/run-invocation.ts +85 -84
  343. package/scripts/run/run-liveness.ts +1 -1
  344. package/scripts/run/run-parent-cut-hook.ts +1 -1
  345. package/scripts/run/run-preflight.ts +6 -6
  346. package/scripts/run/run-prep-cli.ts +1 -1
  347. package/scripts/run/run-prrun-resume.ts +1 -1
  348. package/scripts/run/run-report-cli.ts +4 -4
  349. package/scripts/run/run-report.ts +2 -2
  350. package/scripts/run/run-status-cli.ts +1 -1
  351. package/scripts/run/run-step-cli.ts +3 -3
  352. package/scripts/run/run-step.ts +2 -2
  353. package/scripts/run/run-stop-notify.ts +1 -1
  354. package/scripts/run/run-stranded-detect.ts +4 -4
  355. package/scripts/run/run-stranded.ts +2 -2
  356. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  357. package/scripts/run/run-watcher-guard.ts +5 -3
  358. package/scripts/run/run-watcher-hook.ts +1 -1
  359. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +29 -8
  360. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  361. package/scripts/run/ship/run-ship-lock.ts +50 -0
  362. package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +13 -0
  363. package/scripts/run/ship/run-ship-pre-detach.ts +66 -0
  364. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  365. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  366. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +10 -6
  367. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  368. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  369. package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +13 -0
  370. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +15 -8
  371. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  372. package/scripts/run/ship/run-ship-sync.ts +128 -0
  373. package/scripts/run/{run-ship.ts → ship/run-ship.ts} +6 -0
  374. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  375. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +31 -9
  376. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  377. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +99 -17
  378. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  379. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  380. package/scripts/run/wake/run-wake-restart.ts +58 -0
  381. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  382. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  383. package/scripts/safe-chain/preinstall-command.ts +20 -21
  384. package/scripts/safe-chain/project-config.ts +19 -15
  385. package/scripts/sync/sync-ai-files.ts +1 -1
  386. package/scripts/sync/sync-configs.ts +2 -1
  387. package/scripts/sync/sync.ts +15 -2
  388. package/scripts/test/test-red-logic.ts +17 -2
  389. package/scripts/test/test-red.ts +8 -7
  390. package/scripts/test/unit-worker-share.ts +1 -0
  391. package/scripts/time-runtime/time-background.ts +5 -1
  392. package/scripts/time-runtime/time-instant.ts +3 -1
  393. package/scripts/time-runtime/time-spans.ts +6 -139
  394. package/scripts/time-runtime/time-tool-call.ts +160 -0
  395. package/scripts/time-runtime/time-transcript-line.ts +27 -1
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +17 -17
  398. package/scripts/version/latest-regression.ts +1 -1
  399. package/scripts/version/latest-update.ts +4 -4
  400. package/scripts/version/npm-registry.ts +20 -5
  401. package/scripts/version/publishable-range-check.ts +2 -2
  402. package/scripts/version/publishable-range.ts +1 -1
  403. package/scripts/version/release-age.ts +48 -38
  404. package/scripts/version/release-hold.ts +1 -1
  405. package/scripts/version/safe-chain-version-update.ts +40 -0
  406. package/scripts/version/upgrade-shell-command.ts +48 -8
  407. package/scripts/version/version-check-logic.ts +23 -14
  408. package/scripts/version/version-commands.ts +23 -11
  409. package/scripts/version/version-remote.ts +28 -13
  410. package/templates/pnpm-workspace.basic.yaml +4 -0
  411. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  412. package/templates/workflows/github-release.yml +2 -0
  413. package/scripts/build/build-config-merge.ts +0 -27
  414. package/scripts/build/build-managed-marker.ts +0 -27
  415. package/scripts/build/build-self-sync-guard.ts +0 -27
  416. package/scripts/build/build-version.ts +0 -26
  417. package/scripts/version/preinstall-version-update.ts +0 -86
  418. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  419. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  420. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  421. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  422. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  423. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  424. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  425. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  426. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  427. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  428. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  429. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  430. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  431. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  432. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  433. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  434. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  435. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  436. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  437. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -0,0 +1,39 @@
1
+ import { git_stash } from '#scripts/git/stash/git-stash'
2
+ import { lane_launch_cli, type LaunchOutcome } from '#scripts/lane/lane-launch-cli'
3
+ import { backlog_drive_owner } from './backlog-drive-owner'
4
+
5
+ // The driver's lane launch (joshuafolkken/kit#3272). The parent that ran `josh latest` in the primary
6
+ // checkout leaves its rewritten lock file in a stash under this message — `backlogrun-lanes.md` →
7
+ // "Once per repository, before the first lane opens" — and the lane that pops it carries the update
8
+ // into its pull request. **The stash itself is the "first lane" marker**: each launch asks whether it
9
+ // is still on the stack, and the pop that `lane:launch --stash` runs consumes it, so the first launch
10
+ // after the push takes it and every later one finds nothing. A restarted driver needs no memory of
11
+ // which lane was first. Two entries under the message are a state a person resolves, so neither is
12
+ // guessed at — the launch goes ahead without one, as `stash:pop` would refuse it anyway. That skip is
13
+ // said on stderr, because `stash:pop` refusing would have been heard and a silent skip would let the
14
+ // update never reach a pull request.
15
+ const LATEST_STASH_MESSAGE = 'backlogrun: josh latest before lanes'
16
+
17
+ async function latest_stash(): Promise<string | undefined> {
18
+ const selection = git_stash.select(await git_stash.list(), LATEST_STASH_MESSAGE)
19
+
20
+ if (selection.kind === 'ambiguous') {
21
+ console.error(
22
+ `Launching without the "${LATEST_STASH_MESSAGE}" stash: ${selection.selectors.join(', ')} all match — keep one.`,
23
+ )
24
+ }
25
+
26
+ return selection.kind === 'match' ? LATEST_STASH_MESSAGE : undefined
27
+ }
28
+
29
+ async function launch(issue: string, owner: string): Promise<LaunchOutcome['kind']> {
30
+ await backlog_drive_owner.assert_current(owner)
31
+
32
+ const outcome = await lane_launch_cli.launch_lane({ issue, stash: await latest_stash() })
33
+
34
+ return outcome.kind
35
+ }
36
+
37
+ const backlog_drive_launch = { LATEST_STASH_MESSAGE, launch }
38
+
39
+ export { backlog_drive_launch }
@@ -1,8 +1,8 @@
1
1
  import { EPIC_LABEL, has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
2
2
  import type { IssueState } from '#scripts/issue/issue-state'
3
3
  import { issue_state_cli } from '#scripts/issue/issue-state-cli'
4
- import type { RunCarry } from '#scripts/run/run-carry'
5
- import type { MergeResult } from '#scripts/run/run-merge-cli'
4
+ import type { RunCarry } from '#scripts/run/carry/run-carry'
5
+ import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
6
6
  import { backlog_budget } from './backlog-budget'
7
7
  import type { DriveState, OfferRead } from './backlog-drive'
8
8
  import { backlog_drive_named } from './backlog-drive-named'
@@ -58,9 +58,7 @@ function settled_outcome(state: IssueState): MergeResult['outcome'] | undefined
58
58
  async function classify(issue: string, named: OfferRead, owner: string): Promise<OfferRead> {
59
59
  const result = await issue_state_cli.read_issue(issue)
60
60
 
61
- if (result.kind !== 'state') {
62
- return { verdict: 'issue-state', issues: [], retries: NO_RETRIES }
63
- }
61
+ if (result.kind !== 'state') return { verdict: 'issue-state', issues: [], retries: NO_RETRIES }
64
62
 
65
63
  const outcome = settled_outcome(result.state)
66
64
 
@@ -1,8 +1,8 @@
1
1
  import { has_label_name, NEEDS_DECISION_LABEL } from '#scripts/issue/issue-labels'
2
2
  import { issue_state_cli } from '#scripts/issue/issue-state-cli'
3
- import { run_carry, type RunCarry } from '#scripts/run/run-carry'
4
- import type { RunEvent } from '#scripts/run/run-event-stream'
5
- import type { MergeResult } from '#scripts/run/run-merge-cli'
3
+ import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
4
+ import type { RunEvent } from '#scripts/run/event/run-event-stream'
5
+ import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
6
6
  import type { DriveState, OfferRead } from './backlog-drive'
7
7
  import { backlog_drive_restore } from './backlog-drive-restore'
8
8
  import { backlog_named } from './backlog-named'
@@ -1,4 +1,4 @@
1
- import type { RunCarry } from '#scripts/run/run-carry'
1
+ import type { RunCarry } from '#scripts/run/carry/run-carry'
2
2
  import type { DriveState } from './backlog-drive'
3
3
 
4
4
  const LIST_SEPARATOR = ','
@@ -1,4 +1,4 @@
1
- import { run_carry, type RunCarry } from '#scripts/run/run-carry'
1
+ import { run_carry, type RunCarry } from '#scripts/run/carry/run-carry'
2
2
 
3
3
  function owns_record(carry: RunCarry, owner: string): boolean {
4
4
  const current = run_carry.owner_of(Number(owner))
@@ -1,4 +1,4 @@
1
- import { run_event_stream, type RunEvent } from '#scripts/run/run-event-stream'
1
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
2
2
 
3
3
  const ISSUE_IN_EVENT = /#([1-9]\d*)\b/u
4
4
  const SETTLED_KINDS: ReadonlySet<string> = new Set([
@@ -1,6 +1,6 @@
1
1
  import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
2
2
  import { find_package_directory } from '#scripts/josh/josh-logic'
3
- import type { RunCarry } from '#scripts/run/run-carry'
3
+ import type { RunCarry } from '#scripts/run/carry/run-carry'
4
4
  import { run_retrospective } from '#scripts/run/run-retrospective'
5
5
 
6
6
  // Whether a drain owes the retrospective, read through the owed rule `run:step` reads, so a drain it
@@ -17,7 +17,7 @@
17
17
  // without a process, a network or a clock.
18
18
 
19
19
  import type { LaunchOutcome } from '#scripts/lane/lane-launch-cli'
20
- import type { MergeResult } from '#scripts/run/run-merge-cli'
20
+ import type { MergeResult } from '#scripts/run/merge/run-merge-cli'
21
21
 
22
22
  const RUN_VERDICT = 'run'
23
23
  const STOP_VERDICT = 'stop'
@@ -16,7 +16,8 @@ import { PROJECT_ROOT } from '#scripts/init/init-paths'
16
16
  import { issue_citation } from '#scripts/rules/issue-citation'
17
17
  import { backlog_defect_priority } from './backlog-defect-priority'
18
18
  import { backlog_pool } from './backlog-pool'
19
- import { backlog_rank } from './backlog-rank'
19
+ import { backlog_rank, type GateScope } from './backlog-rank'
20
+ import { backlog_restructure } from './backlog-restructure'
20
21
 
21
22
  // `josh backlog:next` — what the whole opted-in backlog may run next (joshuafolkken/kit#1630).
22
23
  //
@@ -145,6 +146,19 @@ function report(result: EpicNextResult, context: PoolContext): number {
145
146
  return SUCCESS_EXIT_CODE
146
147
  }
147
148
 
149
+ // The settled set the views were classified against, so a standalone row and an epic child weigh the
150
+ // same blocker the same way.
151
+ function running_of(
152
+ views: ReadonlyArray<EpicView>,
153
+ graphs: ReadonlyArray<ReadonlyArray<EpicChild>>,
154
+ context: PoolContext,
155
+ ): ReadonlySet<string> {
156
+ return (
157
+ views[0]?.running ??
158
+ backlog_pool.running_set(graphs, context.opted_in.issues, context.repo, context.tracking.index)
159
+ )
160
+ }
161
+
148
162
  // The two halves as one result. The epic half is already classified by `epic:next`; the standalone
149
163
  // half is classified here; the verdict is decided from the merge, so a backlog with a runnable epic
150
164
  // child and nothing else says `run` exactly as one with a runnable standalone issue does.
@@ -160,10 +174,7 @@ function combine(views: ReadonlyArray<EpicView>, context: PoolContext): EpicNext
160
174
  tracked: epic_index.withheld_children(context.tracking.index, context.opted_in.issues),
161
175
  exclude: context.exclude,
162
176
  repo: context.repo,
163
- // The settled set the views were classified against, so a standalone row and an epic child weigh
164
- // the same blocker the same way.
165
- running:
166
- views[0]?.running ?? backlog_pool.running_set(graphs, context.opted_in.issues, context.repo),
177
+ running: running_of(views, graphs, context),
167
178
  })
168
179
 
169
180
  // The same checkout map `epic:next` hands `build_result`. Without it every bundle heading reads
@@ -193,6 +204,7 @@ function settled_views(
193
204
  views[0]?.running ?? running,
194
205
  context.opted_in.issues,
195
206
  context.repo,
207
+ context.tracking.index,
196
208
  )
197
209
 
198
210
  return settled.size === running.size ? views : settled_views(reads, settled, context)
@@ -211,7 +223,7 @@ function views_from(reads: ReadonlyArray<EpicRead>, context: PoolContext): Reado
211
223
 
212
224
  return settled_views(
213
225
  reads,
214
- backlog_pool.running_set(graphs, context.opted_in.issues, context.repo),
226
+ backlog_pool.running_set(graphs, context.opted_in.issues, context.repo, context.tracking.index),
215
227
  context,
216
228
  )
217
229
  }
@@ -278,7 +290,7 @@ function report_retry(context: PoolContext): number {
278
290
  return SUCCESS_EXIT_CODE
279
291
  }
280
292
 
281
- // The rows the offer's cap bounds — the standalone half, which `backlog:plan --waves` reads too.
293
+ // The rows the offer's cap bounds — the standalone half.
282
294
  function standalone_keys(context: PoolContext): ReadonlySet<string> {
283
295
  return backlog_pool.standalone_keys(context.opted_in.issues, {
284
296
  tracked: epic_index.withheld_children(context.tracking.index, context.opted_in.issues),
@@ -287,6 +299,16 @@ function standalone_keys(context: PoolContext): ReadonlySet<string> {
287
299
  })
288
300
  }
289
301
 
302
+ // The running lanes still carry the pickup label, so the opted-in listing holds their bodies too —
303
+ // which is what lets a candidate be held back from a restructure a running lane already makes
304
+ // (joshuafolkken/kit#3221). `backlog:plan --waves` reads the same scope, so its first wave is this offer.
305
+ function gate_scope(context: PoolContext): GateScope {
306
+ return {
307
+ standalone: standalone_keys(context),
308
+ declared: backlog_restructure.declared_of(context.opted_in.issues),
309
+ }
310
+ }
311
+
290
312
  // The `run:solo` gate is applied here rather than in `resolve`, because `backlog:plan` shares
291
313
  // `resolve` and a plan made before the run lists everything (joshuafolkken/kit#2776). The holders are
292
314
  // read only on a `run` answer — the one answer the gate can change. The cap follows the gate
@@ -302,7 +324,7 @@ async function gate_solo(result: EpicNextResult, context: PoolContext): Promise<
302
324
 
303
325
  if (notice !== undefined) console.error(notice)
304
326
 
305
- const gated = backlog_rank.gate(result, read, repo, standalone_keys(context))
327
+ const gated = backlog_rank.gate(result, read, repo, gate_scope(context))
306
328
 
307
329
  if (gated.notice !== undefined) console.error(gated.notice)
308
330
 
@@ -418,7 +440,7 @@ const backlog_next = {
418
440
  READ_FAILURES,
419
441
  VERDICT_TOKENS,
420
442
  resolve,
421
- standalone_keys,
443
+ gate_scope,
422
444
  context_of,
423
445
  run,
424
446
  }
@@ -3,8 +3,8 @@ import { fileURLToPath } from 'node:url'
3
3
  import { josh_command } from '#scripts/josh/josh-run'
4
4
  import { cli_flags } from '#scripts/lib/cli-flags'
5
5
  import { rule_value_cache } from '#scripts/rules/rule-value-cache'
6
- import { run_event_stream } from '#scripts/run/run-event-stream'
7
- import { run_event_stream_emit } from '#scripts/run/run-event-stream-emit'
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
8
  import { z } from 'zod'
9
9
  import { backlog_offer, type OfferAnswer } from './backlog-offer'
10
10
 
@@ -74,8 +74,8 @@ function to_count(raw: string | undefined): number | undefined {
74
74
 
75
75
  // A count flag that was given but unreadable makes the invocation unreadable rather than defaulted —
76
76
  // the contract `backlog:budget` follows for its own counts.
77
- function count_of(raw: string | undefined, fallback: number): number | undefined {
78
- return raw === undefined ? fallback : to_count(raw)
77
+ function count_of(raw: string | undefined): number | undefined {
78
+ return raw === undefined ? DEFAULT_COUNT : to_count(raw)
79
79
  }
80
80
 
81
81
  interface OfferCounts {
@@ -86,8 +86,8 @@ interface OfferCounts {
86
86
  // The two counts the mapping needs: `--running` decides `wait`, `--retries` decides `retry`. Either
87
87
  // given but unreadable refuses the whole invocation.
88
88
  function counts_of(values: ParsedValues): OfferCounts | undefined {
89
- const running = count_of(cli_flags.string_of(values.running), DEFAULT_COUNT)
90
- const retries = count_of(cli_flags.string_of(values.retries), DEFAULT_COUNT)
89
+ const running = count_of(cli_flags.string_of(values.running))
90
+ const retries = count_of(cli_flags.string_of(values.retries))
91
91
 
92
92
  if (running === undefined || retries === undefined) return undefined
93
93
 
@@ -5,7 +5,7 @@ import { backlog_next } from './backlog-next'
5
5
  // answered — its stdout tokens and its exit code — into the one word `backlog:budget` is then asked
6
6
  // with, plus the issue numbers to start and the consecutive-retry count carried to the next ask.
7
7
  //
8
- // **The mapping is `docs/josh-commands-automation.md` → "`josh backlog:offer`", held here so it cannot
8
+ // **The mapping is `docs/josh-commands-backlog.md` → "`josh backlog:offer`", held here so it cannot
9
9
  // drift.** It fixes which `backlog:next` answer becomes which `backlog:budget` word, and it carries two
10
10
  // context branches an agent once applied by hand every iteration: `wait` is `blocked` while this run
11
11
  // has children in flight and `exhausted` when it has none, and `retry` is `blocked` until the third
@@ -4,12 +4,13 @@ import { auto_ok_cli } from '#scripts/auto-ok/auto-ok-cli'
4
4
  import { epic_index } from '#scripts/epic/epic-index'
5
5
  import type { EpicNextResult } from '#scripts/epic/epic-report'
6
6
  import { git_gh_command } from '#scripts/gh/git-gh-command'
7
+ import { open_issue_schema, type OpenIssueData } from '#scripts/git/git-schemas'
7
8
  import { read_json_listing } from '#scripts/git/parse-json-array'
8
- import { open_issue_schema, type OpenIssueData } from '#scripts/git/schemas'
9
9
  import { run_invocation } from '#scripts/run/run-invocation'
10
10
  import { backlog_named } from './backlog-named'
11
11
  import { backlog_next, type OptedIn } from './backlog-next'
12
12
  import { backlog_plan, type NamedPlan, type PlanContext } from './backlog-plan'
13
+ import type { GateScope } from './backlog-rank'
13
14
  import { backlog_scope } from './backlog-scope'
14
15
  import { backlog_waves } from './backlog-waves'
15
16
 
@@ -104,9 +105,9 @@ interface Plan {
104
105
  // past the listing cap. Carrying the *whole* index here instead would move that misreport rather
105
106
  // than remove it: the sentence would name an epic that is withholding nothing.
106
107
  tracked: ReadonlyMap<number, number>
107
- // The rows the offer's cap bounds, so `--waves` cuts each wave where `backlog:next` does
108
- // (joshuafolkken/kit#2928).
109
- standalone: ReadonlySet<string>
108
+ // The rows the offer's cap bounds and the paths each issue restructures, so `--waves` cuts each
109
+ // wave where `backlog:next` does (joshuafolkken/kit#2928, joshuafolkken/kit#3221).
110
+ scope: GateScope
110
111
  }
111
112
 
112
113
  // The listing plus whether it was cut. Carried together because the rows alone cannot say whether
@@ -141,7 +142,7 @@ async function classify(
141
142
  repo: context.repo,
142
143
  exclude,
143
144
  tracked: epic_index.withheld_children(context.tracking.index, context.opted_in.issues),
144
- standalone: backlog_next.standalone_keys(context),
145
+ scope: backlog_next.gate_scope(context),
145
146
  }
146
147
  }
147
148
 
@@ -172,7 +173,7 @@ function print_plan(plan: Plan, listing: OpenListing, view: PlanView): void {
172
173
  const context = context_of(plan, listing)
173
174
 
174
175
  if (view.is_waves) {
175
- console.info(backlog_waves.format_waves(plan.result, context, plan.standalone))
176
+ console.info(backlog_waves.format_waves(plan.result, context, plan.scope))
176
177
 
177
178
  return
178
179
  }
@@ -5,7 +5,7 @@ import { epic_index } from '#scripts/epic/epic-index'
5
5
  import { epic_issue } from '#scripts/epic/epic-issue'
6
6
  import type { EpicView } from '#scripts/epic/epic-next-views'
7
7
  import { epic_outside_blocker } from '#scripts/epic/epic-outside-blocker'
8
- import type { OpenIssueData } from '#scripts/git/schemas'
8
+ import type { OpenIssueData } from '#scripts/git/git-schemas'
9
9
  import { git_next_issues } from '#scripts/issue/git-next-issues'
10
10
  import {
11
11
  ALREADY_DONE_LABEL,
@@ -37,6 +37,9 @@ const DECISION_LABELS: ReadonlySet<string> = new Set([NEEDS_DECISION_LABEL, ALRE
37
37
  // `tracked` is the narrowed set `epic_index.withheld_children` builds, not every child an epic
38
38
  // tracks (joshuafolkken/kit#1668) — the narrowing itself is defined there, once, for this half and
39
39
  // `auto-ok:next` alike.
40
+ // Child number to every open epic whose task list names it — `epic_index.build_tracking_index`.
41
+ type TrackingIndex = ReadonlyMap<number, ReadonlyArray<number>>
42
+
40
43
  // The part of the context that decides which rows the standalone half considers at all.
41
44
  type ConsideredContext = Pick<StandaloneContext, 'tracked' | 'exclude' | 'repo'>
42
45
 
@@ -69,24 +72,64 @@ function needs_person(issue: OpenIssueData, context: StandaloneContext): boolean
69
72
  )
70
73
  }
71
74
 
75
+ // Each open epic's key, read off the tracking index — child number to the epics listing it.
76
+ function epic_keys_of(tracking: TrackingIndex, repo: string): ReadonlySet<string> {
77
+ return new Set([...tracking.values()].flat().map((number) => epic_graph.key_of({ repo, number })))
78
+ }
79
+
80
+ // The set with every epic that tracks a member added, nested epics included (joshuafolkken/kit#3289).
81
+ // An epic is a container rather than work, so it is never run itself — but it closes once its children
82
+ // do, so a row blocked by it waits on time exactly when one of those children is in this run.
83
+ function with_tracking_epics(
84
+ running: ReadonlySet<string>,
85
+ tracking: TrackingIndex,
86
+ repo: string,
87
+ ): ReadonlySet<string> {
88
+ const tracking_epics = [...tracking]
89
+ .filter(([child]) => running.has(epic_graph.key_of({ repo, number: child })))
90
+ .flatMap(([, epics]) => epics.map((number) => epic_graph.key_of({ repo, number })))
91
+ const grown = new Set([...running, ...tracking_epics])
92
+
93
+ return grown.size === running.size ? running : with_tracking_epics(grown, tracking, repo)
94
+ }
95
+
96
+ // The epics re-derived from the members alone, so an epic whose last running child was just taken out
97
+ // leaves with it rather than lingering from an earlier pass.
98
+ function rooted(
99
+ running: ReadonlySet<string>,
100
+ tracking: TrackingIndex,
101
+ repo: string,
102
+ ): ReadonlySet<string> {
103
+ const epics = epic_keys_of(tracking, repo)
104
+ const members = new Set([...running].filter((key) => !epics.has(key)))
105
+
106
+ return with_tracking_epics(members, tracking, repo)
107
+ }
108
+
109
+ function is_stuck(issue: OpenIssueData, running: ReadonlySet<string>, repo: string): boolean {
110
+ return has_any_label(issue.labels, DECISION_LABELS) || waits_outside(issue, running, repo)
111
+ }
112
+
72
113
  // The running set with every standalone row a person has to resolve taken out, repeated until a pass
73
114
  // removes nothing — a row waiting on such a row waits on that person too, however long the chain.
74
115
  function settle_standalone(
75
116
  running: ReadonlySet<string>,
76
117
  issues: ReadonlyArray<OpenIssueData>,
77
118
  repo: string,
119
+ tracking: TrackingIndex,
78
120
  ): ReadonlySet<string> {
79
121
  const settled = new Set(running)
80
122
 
81
- // An epic row is never in the set, so deleting its key changes nothing and it needs no filter.
123
+ // An epic row is never a standalone row, so deleting its key here never happens; `rooted` decides it.
82
124
  for (const issue of issues) {
83
- const is_stuck =
84
- has_any_label(issue.labels, DECISION_LABELS) || waits_outside(issue, running, repo)
85
-
86
- if (is_stuck) settled.delete(epic_graph.key_of({ repo, number: issue.number }))
125
+ if (is_stuck(issue, running, repo)) {
126
+ settled.delete(epic_graph.key_of({ repo, number: issue.number }))
127
+ }
87
128
  }
88
129
 
89
- return settled.size === running.size ? running : settle_standalone(settled, issues, repo)
130
+ const next = rooted(settled, tracking, repo)
131
+
132
+ return next.size === running.size ? next : settle_standalone(next, issues, repo, tracking)
90
133
  }
91
134
 
92
135
  // An opted-in row as the graph's node type. Both halves of the pool are then the same shape, so one
@@ -182,18 +225,21 @@ function classify_standalone(
182
225
  // (joshuafolkken/kit#1943). A child is opted in through its epic's `auto-ok` even where it carries none
183
226
  // of its own, which is why the set is built from the graphs that were read rather than from labels. A
184
227
  // standalone row a person has to resolve is left out, so a row waiting on it waits for that person too;
185
- // an epic child in the same state is taken out by `epic_next_views.settle_views`.
228
+ // an epic child in the same state is taken out by `epic_next_views.settle_views`. An open epic with a
229
+ // child in the set is in it too, so a row blocked by that epic waits rather than needs a person. A
230
+ // closed child is left out: it runs nothing, and kept in the set it would hold its epic there.
186
231
  function running_set(
187
232
  graphs: ReadonlyArray<ReadonlyArray<EpicChild>>,
188
233
  issues: ReadonlyArray<OpenIssueData>,
189
234
  repo: string,
235
+ tracking: TrackingIndex,
190
236
  ): ReadonlySet<string> {
191
237
  const running = epic_outside_blocker.running_keys([
192
- ...graphs.flat(),
238
+ ...graphs.flat().filter((child) => child.state === 'OPEN'),
193
239
  ...to_children(standalone_rows(issues), repo),
194
240
  ])
195
241
 
196
- return settle_standalone(running, issues, repo)
242
+ return settle_standalone(rooted(running, tracking, repo), issues, repo, tracking)
197
243
  }
198
244
 
199
245
  // A cycle no single epic can see, because its links cross from one graph into another. Each epic's own
@@ -336,4 +382,4 @@ const backlog_pool = {
336
382
  }
337
383
 
338
384
  export { backlog_pool }
339
- export type { ConsideredContext, StandaloneContext }
385
+ export type { ConsideredContext, StandaloneContext, TrackingIndex }
@@ -4,6 +4,7 @@ import { epic_rank } from '#scripts/epic/epic-rank'
4
4
  import type { EpicNextResult } from '#scripts/epic/epic-report'
5
5
  import { epic_solo, type SoloGate, type SoloSelection } from '#scripts/epic/epic-solo'
6
6
  import { git_next_issues } from '#scripts/issue/git-next-issues'
7
+ import { backlog_restructure, type Declared } from './backlog-restructure'
7
8
 
8
9
  // What the backlog offers, in the order it is decided (joshuafolkken/kit#2928): rank, then the
9
10
  // `run:solo` gate, then the cap. `backlog:next` and `backlog:plan --waves` both call `select`, so the
@@ -78,29 +79,37 @@ interface OfferInput {
78
79
  repo: string
79
80
  // The keys of the standalone rows — the ones the cap bounds.
80
81
  standalone: ReadonlySet<string>
82
+ // The paths each issue restructures; two candidates claiming one are not offered together
83
+ // (joshuafolkken/kit#3221). Absent, nothing is separated.
84
+ declared?: Declared
81
85
  }
82
86
 
87
+ // The restructure separation runs before the cap, for the reason the gate does: the cap only ever
88
+ // removes what would otherwise be offered.
83
89
  function select(input: OfferInput): SoloSelection {
84
90
  const ranked = rank(input.candidates, input.pool)
91
+ const { read, repo } = input
92
+ const gated = epic_solo.select(ranked, read, repo)
93
+ const separated = backlog_restructure.separate(gated, input.declared ?? new Map(), { read, repo })
85
94
 
86
- return cap(epic_solo.select(ranked, input.read, input.repo), input.standalone)
95
+ return cap(separated, input.standalone)
96
+ }
97
+
98
+ interface GateScope {
99
+ standalone: ReadonlySet<string>
100
+ declared: Declared
87
101
  }
88
102
 
89
103
  // `backlog:next`'s gate: `epic_solo.gate` with this selection in place of its bare one.
90
- function gate(
91
- result: EpicNextResult,
92
- read: BusyRead,
93
- repo: string,
94
- standalone: ReadonlySet<string>,
95
- ): SoloGate {
104
+ function gate(result: EpicNextResult, read: BusyRead, repo: string, scope: GateScope): SoloGate {
96
105
  const pool = everything_in(result)
97
106
 
98
107
  return epic_solo.gate(result, read, repo, (candidates) =>
99
- select({ candidates, pool, read, repo, standalone }),
108
+ select({ candidates, pool, read, repo, ...scope }),
100
109
  )
101
110
  }
102
111
 
103
112
  const backlog_rank = { everything_in, gate, rank_result, select }
104
113
 
105
114
  export { backlog_rank }
106
- export type { OfferInput }
115
+ export type { GateScope, OfferInput }
@@ -3,7 +3,7 @@ import { issue_cite } from '#scripts/issue/issue-cite'
3
3
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
4
4
  import { lane_capacity } from '#scripts/lane/lane-capacity'
5
5
  import { lane_registry } from '#scripts/lane/lane-registry'
6
- import type { RunCarry } from '#scripts/run/run-carry'
6
+ import type { RunCarry } from '#scripts/run/carry/run-carry'
7
7
  import { run_headless } from '#scripts/run/run-headless'
8
8
  import { run_invocation } from '#scripts/run/run-invocation'
9
9
  import { backlog_stalled } from './backlog-stalled'
@@ -0,0 +1,152 @@
1
+ import type { BusyRead } from '#scripts/epic/epic-busy'
2
+ import type { EpicChild } from '#scripts/epic/epic-graph'
3
+ import type { SoloSelection } from '#scripts/epic/epic-solo'
4
+
5
+ // Which candidates may not run side by side because both restructure the same file
6
+ // (joshuafolkken/kit#3221). The condition is deliberately narrow: plain overlap is not serialized,
7
+ // because every resume record of the 2026-10-05 backlog run was resolved mechanically by merging
8
+ // main. What git cannot resolve on its own is a rename or modify/delete conflict — two lanes that
9
+ // each move, split, rename or delete the same file — so only a path both bodies name on a line with
10
+ // such a verb holds the second one back. `remove` is left out: issue bodies use it for lines and
11
+ // options far more often than for files.
12
+ const RESTRUCTURE_WORDS: ReadonlySet<string> = new Set([
13
+ 'move',
14
+ 'moved',
15
+ 'moves',
16
+ 'moving',
17
+ 'rename',
18
+ 'renamed',
19
+ 'renames',
20
+ 'renaming',
21
+ 'split',
22
+ 'splits',
23
+ 'splitting',
24
+ 'delete',
25
+ 'deleted',
26
+ 'deletes',
27
+ 'deleting',
28
+ ])
29
+ const RESTRUCTURE_PHRASES: ReadonlyArray<string> = ['移動', '分割', '削除', 'リネーム', '改名']
30
+ const WORD = /[a-z]+/giu
31
+ const CODE_SPAN = /`([^`\s]+)`/gu
32
+ const LINE_SUFFIX = /:\d+(?::\d+)?$/u
33
+ const FILE_EXTENSION = /\.[a-z]\w*$/iu
34
+
35
+ // The restructured paths each issue declares, by issue number.
36
+ type Declared = ReadonlyMap<number, ReadonlySet<string>>
37
+
38
+ interface DeclaringIssue {
39
+ number: number
40
+ body?: string | undefined
41
+ }
42
+
43
+ function is_path(token: string): boolean {
44
+ return token.includes('/') || FILE_EXTENSION.test(token)
45
+ }
46
+
47
+ function paths_on(line: string): Array<string> {
48
+ return [...line.matchAll(CODE_SPAN)]
49
+ .map((match) => (match[1] ?? '').replace(LINE_SUFFIX, ''))
50
+ .filter((token) => is_path(token))
51
+ }
52
+
53
+ function is_restructuring(line: string): boolean {
54
+ const words = line.match(WORD) ?? []
55
+
56
+ if (words.some((word) => RESTRUCTURE_WORDS.has(word.toLowerCase()))) return true
57
+
58
+ return RESTRUCTURE_PHRASES.some((phrase) => line.includes(phrase))
59
+ }
60
+
61
+ // The backticked paths on the lines of `body` that carry a restructuring verb.
62
+ function restructured_paths(body: string): ReadonlySet<string> {
63
+ const lines = body.split('\n').filter((line) => is_restructuring(line))
64
+
65
+ return new Set(lines.flatMap((line) => paths_on(line)))
66
+ }
67
+
68
+ function declared_of(issues: ReadonlyArray<DeclaringIssue>): Declared {
69
+ return new Map(issues.map((issue) => [issue.number, restructured_paths(issue.body ?? '')]))
70
+ }
71
+
72
+ interface SeparateContext {
73
+ read: BusyRead
74
+ repo: string
75
+ }
76
+
77
+ // The path each running lane restructures, mapped to the issue that holds it.
78
+ function running_claims(declared: Declared, read: BusyRead): Map<string, number> {
79
+ const holders = read.kind === 'busy' ? read.issues.map((issue) => issue.number) : []
80
+ const pairs = holders.flatMap((number) =>
81
+ [...(declared.get(number) ?? [])].map((path): [string, number] => [path, number]),
82
+ )
83
+
84
+ return new Map(pairs)
85
+ }
86
+
87
+ function paths_of(child: EpicChild, declared: Declared, repo: string): ReadonlySet<string> {
88
+ if (child.repo !== repo) return new Set()
89
+
90
+ return declared.get(child.number) ?? new Set()
91
+ }
92
+
93
+ function clash_note(child: EpicChild, path: string, holder: number): string {
94
+ return `#${String(child.number)} waits: it and #${String(holder)} both restructure \`${path}\`.`
95
+ }
96
+
97
+ interface Walk {
98
+ claims: Map<string, number>
99
+ offered: Array<EpicChild>
100
+ withheld: Array<EpicChild>
101
+ notes: Array<string>
102
+ }
103
+
104
+ function visit(walk: Walk, child: EpicChild, paths: ReadonlySet<string>): void {
105
+ const clash = [...paths].find((path) => walk.claims.has(path))
106
+
107
+ if (clash === undefined) {
108
+ walk.offered.push(child)
109
+ for (const path of paths) walk.claims.set(path, child.number)
110
+
111
+ return
112
+ }
113
+
114
+ walk.withheld.push(child)
115
+ walk.notes.push(clash_note(child, clash, walk.claims.get(clash) ?? child.number))
116
+ }
117
+
118
+ function merged_notice(
119
+ notice: string | undefined,
120
+ notes: ReadonlyArray<string>,
121
+ ): string | undefined {
122
+ const parts = notice === undefined ? notes : [notice, ...notes]
123
+
124
+ return parts.length === 0 ? undefined : parts.join('\n')
125
+ }
126
+
127
+ // `selection` with every offered candidate that restructures a path a running lane or an earlier
128
+ // candidate already restructures moved to `withheld`, in offer order.
129
+ function separate(
130
+ selection: SoloSelection,
131
+ declared: Declared,
132
+ context: SeparateContext,
133
+ ): SoloSelection {
134
+ const walk: Walk = {
135
+ claims: running_claims(declared, context.read),
136
+ offered: [],
137
+ withheld: [],
138
+ notes: [],
139
+ }
140
+
141
+ for (const child of selection.offered) visit(walk, child, paths_of(child, declared, context.repo))
142
+
143
+ const notice = merged_notice(selection.notice, walk.notes)
144
+ const withheld = [...walk.withheld, ...selection.withheld]
145
+
146
+ return { offered: walk.offered, withheld, ...(notice !== undefined && { notice }) }
147
+ }
148
+
149
+ const backlog_restructure = { declared_of, restructured_paths, separate }
150
+
151
+ export { backlog_restructure }
152
+ export type { Declared }
@@ -1,5 +1,5 @@
1
1
  import type { EpicNextResult } from '#scripts/epic/epic-report'
2
- import type { OpenIssueData } from '#scripts/git/schemas'
2
+ import type { OpenIssueData } from '#scripts/git/git-schemas'
3
3
  import { AUTO_OK_LABEL, EPIC_LABEL, has_any_label } from '#scripts/issue/issue-labels'
4
4
 
5
5
  // Which open issues the backlog will not run, and why (joshuafolkken/kit#1652).