@joshuafolkken/kit 1.1178.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 (425) 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 +6 -2
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  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 +21 -13
  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 +6 -1
  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 +32 -31
  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-S6X6WYKR.js → chunk-55HHG5PX.js} +1210 -711
  43. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-32GQTL4F.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-VESKRQOF.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-JUDKDCDP.js} +496 -378
  52. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-NV2JRZZO.js} +718 -681
  53. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-I4XSL4TT.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 +217 -195
  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 +66 -58
  64. package/dist/josh.js +868 -74
  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 +16 -5
  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 +1 -1
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-ready.ts +1 -1
  113. package/scripts/backlog/backlog-scope.ts +1 -1
  114. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  115. package/scripts/backlog/backlog-stalled.ts +1 -1
  116. package/scripts/build/build-codex-hooks.ts +3 -1
  117. package/scripts/build/build-commands.ts +66 -0
  118. package/scripts/build/build-hooks.ts +2 -1
  119. package/scripts/build/build-library.ts +12 -3
  120. package/scripts/build/build.ts +70 -0
  121. package/scripts/build/local-bin.ts +3 -13
  122. package/scripts/delegation/investigation-reads.ts +3 -1
  123. package/scripts/discovery/repo-origin.ts +5 -5
  124. package/scripts/doctor/doctor-consumer.ts +2 -2
  125. package/scripts/doctor/doctor-io.ts +2 -3
  126. package/scripts/document/capped-print-part.ts +30 -0
  127. package/scripts/document/capped-print.ts +3 -4
  128. package/scripts/document/document-byte-budget.ts +11 -2
  129. package/scripts/document/document-scan.ts +117 -0
  130. package/scripts/document/document-section.ts +143 -16
  131. package/scripts/document/entry-read-budget.ts +5 -3
  132. package/scripts/document/entry-read-set.ts +10 -89
  133. package/scripts/document/read-set-cost.ts +98 -0
  134. package/scripts/epic/epic-add-body.ts +1 -0
  135. package/scripts/epic/epic-audit-checks.ts +1 -1
  136. package/scripts/epic/epic-audit-rationale.ts +4 -3
  137. package/scripts/epic/epic-bundle-cli.ts +14 -3
  138. package/scripts/epic/epic-bundle-poll.ts +41 -0
  139. package/scripts/epic/epic-busy.ts +1 -1
  140. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  141. package/scripts/epic/epic-classify.ts +1 -1
  142. package/scripts/epic/epic-cli-argv.ts +78 -46
  143. package/scripts/epic/epic-cli-remove.ts +11 -19
  144. package/scripts/epic/epic-cli.ts +74 -73
  145. package/scripts/epic/epic-close-comment.ts +1 -1
  146. package/scripts/epic/epic-close.ts +1 -1
  147. package/scripts/epic/epic-cross-repo.ts +5 -5
  148. package/scripts/epic/epic-graph.ts +1 -1
  149. package/scripts/epic/epic-index.ts +1 -1
  150. package/scripts/epic/epic-issue.ts +2 -6
  151. package/scripts/epic/epic-lane-offer.ts +2 -2
  152. package/scripts/epic/epic-next.ts +29 -42
  153. package/scripts/epic/epic-solo-stale.ts +1 -1
  154. package/scripts/epic/epic-triage.ts +8 -4
  155. package/scripts/epic/epic-validate.ts +1 -1
  156. package/scripts/followup/git-followup-finish.ts +2 -2
  157. package/scripts/followup/git-followup-label.ts +2 -2
  158. package/scripts/followup/git-followup-merged.ts +2 -2
  159. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  160. package/scripts/followup/git-pr-followup.ts +4 -129
  161. package/scripts/gate/core-budget.ts +69 -1
  162. package/scripts/gate/gate-plan.ts +47 -7
  163. package/scripts/gate/gate-skip.ts +38 -7
  164. package/scripts/gate/hook-gate-reuse.ts +12 -10
  165. package/scripts/gate/pre-commit-type-check.ts +4 -1
  166. package/scripts/gate/project-checks.ts +6 -9
  167. package/scripts/gate/type-check-step.ts +2 -3
  168. package/scripts/gate/verification-gate.ts +27 -8
  169. package/scripts/gh/git-gh-api-path.ts +2 -2
  170. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  171. package/scripts/gh/git-gh-issue-write.ts +53 -7
  172. package/scripts/gh/git-gh-pr-read.ts +64 -19
  173. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  174. package/scripts/gh/git-gh-pr.ts +5 -1
  175. package/scripts/gh/git-pr-ai-review.ts +1 -1
  176. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  177. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  178. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  179. package/scripts/gh/git-pr-checks.ts +7 -5
  180. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  181. package/scripts/gh/git-pr-error.ts +1 -3
  182. package/scripts/gh/git-pr.ts +9 -13
  183. package/scripts/gh/github-issue-url.ts +2 -2
  184. package/scripts/git/constants.ts +2 -6
  185. package/scripts/git/git-command.ts +20 -218
  186. package/scripts/git/git-diff-reads.ts +225 -0
  187. package/scripts/git/git-error.ts +4 -12
  188. package/scripts/git/git-pre-push-hook.ts +139 -0
  189. package/scripts/git/git-prompt.ts +2 -6
  190. package/scripts/git/git-spawn-sync.ts +3 -1
  191. package/scripts/git/git-spawn.ts +1 -1
  192. package/scripts/git/git-staging.ts +4 -6
  193. package/scripts/git/git-status.ts +3 -9
  194. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  195. package/scripts/hooks/edited-cspell.ts +2 -4
  196. package/scripts/hooks/format-edited-file.ts +3 -12
  197. package/scripts/hooks/pre-push-unit.ts +31 -9
  198. package/scripts/hooks/reserved-run.ts +2 -2
  199. package/scripts/hooks/run-hook.sh +5 -4
  200. package/scripts/hooks/stop-guard.ts +5 -3
  201. package/scripts/init/init-actions.ts +0 -5
  202. package/scripts/init/init-ai-copy.ts +13 -2
  203. package/scripts/init/init-logic-json-merge.ts +19 -0
  204. package/scripts/init/init-logic-secretlint.ts +7 -7
  205. package/scripts/init/init-logic-workspace.ts +41 -2
  206. package/scripts/init/init-logic.ts +16 -26
  207. package/scripts/init/init.ts +5 -15
  208. package/scripts/init/initial-commit.ts +4 -4
  209. package/scripts/init/kit-development-versions.ts +40 -0
  210. package/scripts/init/kit-setup-state.ts +2 -2
  211. package/scripts/init/start-exec.ts +68 -19
  212. package/scripts/init/start-setup-pr.ts +9 -9
  213. package/scripts/init/start-steps.ts +6 -10
  214. package/scripts/issue/defect-rate-cli.ts +1 -1
  215. package/scripts/issue/git-next-issues.ts +1 -1
  216. package/scripts/issue/issue-auto-ok.ts +1 -1
  217. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  218. package/scripts/issue/issue-backlinks.ts +9 -9
  219. package/scripts/issue/issue-comment-cli.ts +19 -13
  220. package/scripts/issue/issue-file-cli.ts +45 -18
  221. package/scripts/issue/issue-file.ts +24 -1
  222. package/scripts/issue/issue-scout-closed.ts +1 -1
  223. package/scripts/issue/issue-state-cli.ts +30 -74
  224. package/scripts/josh/josh-command-map.ts +2 -0
  225. package/scripts/josh/josh-command-types.ts +11 -0
  226. package/scripts/josh/josh-commands-ai.ts +4 -4
  227. package/scripts/josh/josh-commands-backlog.ts +4 -1
  228. package/scripts/josh/josh-commands-development.ts +10 -0
  229. package/scripts/josh/josh-commands-document.ts +1 -0
  230. package/scripts/josh/josh-commands-lane.ts +22 -0
  231. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  232. package/scripts/josh/josh-commands-run.ts +12 -7
  233. package/scripts/josh/josh-in-process.ts +28 -9
  234. package/scripts/josh/josh-logic.ts +33 -15
  235. package/scripts/josh/process-identity.ts +1 -3
  236. package/scripts/josh/session-language-cli.ts +3 -4
  237. package/scripts/lane/lane-await.ts +5 -1
  238. package/scripts/lane/lane-child-invocation.ts +2 -2
  239. package/scripts/lane/lane-cli.ts +3 -3
  240. package/scripts/lane/lane-dispatch.ts +14 -10
  241. package/scripts/lane/lane-environment.ts +5 -1
  242. package/scripts/lane/lane-handoff.ts +1 -1
  243. package/scripts/lane/lane-install.ts +2 -13
  244. package/scripts/lane/lane-launch-cli.ts +15 -1
  245. package/scripts/lane/lane-ledger.ts +145 -0
  246. package/scripts/lane/lane-leftover.ts +1 -1
  247. package/scripts/lane/lane-load.ts +108 -0
  248. package/scripts/lane/lane-measure-cli.ts +149 -0
  249. package/scripts/lane/lane-occupancy.ts +3 -0
  250. package/scripts/lane/lane-reap.ts +1 -3
  251. package/scripts/lane/lane-relaunch.ts +1 -1
  252. package/scripts/lane/lane-stats.ts +224 -0
  253. package/scripts/lane/lane-vacant.ts +36 -0
  254. package/scripts/lane/openai-lane-ship.ts +2 -2
  255. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  256. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  257. package/scripts/lane/openai-review-broker.ts +2 -4
  258. package/scripts/lib/ancestor-directories.ts +35 -0
  259. package/scripts/lib/animation-helpers.ts +2 -2
  260. package/scripts/lib/buffered-process.ts +3 -7
  261. package/scripts/lib/cli-flags.ts +71 -1
  262. package/scripts/lib/spawn-exit.ts +2 -2
  263. package/scripts/lib/timeouts.ts +35 -2
  264. package/scripts/lines/line-budget.ts +4 -6
  265. package/scripts/notify/telegram-test.ts +6 -0
  266. package/scripts/observations/observation-ledger-line.ts +6 -6
  267. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  268. package/scripts/observations/observation-ledger.ts +26 -7
  269. package/scripts/overrides/lockfile-overrides.ts +1 -1
  270. package/scripts/overrides/overrides-logic.ts +1 -1
  271. package/scripts/propagate/propagate-git.ts +1 -1
  272. package/scripts/propagate/propagate-publish.ts +2 -2
  273. package/scripts/propagate/propagate-steps.ts +20 -15
  274. package/scripts/propagate/propagate.ts +36 -27
  275. package/scripts/refactor/import-graph.ts +18 -1
  276. package/scripts/refactor/refactor-lint.ts +4 -4
  277. package/scripts/repo/repository-labels.ts +2 -2
  278. package/scripts/repo/required-checks-logic.ts +1 -3
  279. package/scripts/review/live-evidence.ts +2 -4
  280. package/scripts/review/review-brief-cli.ts +1 -1
  281. package/scripts/review/review-finding-ledger.ts +1 -1
  282. package/scripts/review/review-findings-cli.ts +1 -3
  283. package/scripts/rules/decision-oracle-batch.ts +128 -0
  284. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  285. package/scripts/rules/decision-oracle.ts +8 -120
  286. package/scripts/rules/delivered-rules.ts +26 -182
  287. package/scripts/rules/direct-filing.ts +1 -1
  288. package/scripts/rules/early-heartbeat.ts +3 -3
  289. package/scripts/rules/git-argv.ts +1 -1
  290. package/scripts/rules/git-force.ts +1 -1
  291. package/scripts/rules/implementation-cut.ts +2 -2
  292. package/scripts/rules/index-guard.ts +1 -1
  293. package/scripts/rules/issue-comments.ts +118 -0
  294. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  295. package/scripts/rules/lane-background.ts +2 -2
  296. package/scripts/rules/lane-park.ts +18 -0
  297. package/scripts/rules/lane-split-park.ts +128 -0
  298. package/scripts/rules/oracle-firing.ts +1 -1
  299. package/scripts/rules/poll-loop.ts +1 -1
  300. package/scripts/rules/pre-gate-cut.ts +1 -1
  301. package/scripts/rules/protected-files.ts +59 -4
  302. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  303. package/scripts/rules/shell-body-trigger.ts +1 -1
  304. package/scripts/rules/stop-rules.ts +18 -7
  305. package/scripts/rules/test-declared-commit.ts +4 -4
  306. package/scripts/rules/wip-cap.ts +63 -0
  307. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  308. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  309. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  310. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  311. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  312. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +2 -2
  313. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  314. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  315. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  316. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  317. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  318. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  319. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  320. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  321. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  322. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  323. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  324. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  325. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  326. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  327. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  328. package/scripts/run/run-ending.ts +2 -2
  329. package/scripts/run/run-halfrun-resume.ts +6 -1
  330. package/scripts/run/run-headless.ts +1 -1
  331. package/scripts/run/run-invocation.ts +85 -84
  332. package/scripts/run/run-liveness.ts +1 -1
  333. package/scripts/run/run-parent-cut-hook.ts +1 -1
  334. package/scripts/run/run-preflight.ts +6 -6
  335. package/scripts/run/run-prep-cli.ts +1 -1
  336. package/scripts/run/run-prrun-resume.ts +1 -1
  337. package/scripts/run/run-report-cli.ts +4 -4
  338. package/scripts/run/run-report.ts +2 -2
  339. package/scripts/run/run-status-cli.ts +1 -1
  340. package/scripts/run/run-step-cli.ts +3 -3
  341. package/scripts/run/run-step.ts +2 -2
  342. package/scripts/run/run-stop-notify.ts +1 -1
  343. package/scripts/run/run-stranded-detect.ts +4 -4
  344. package/scripts/run/run-stranded.ts +2 -2
  345. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  346. package/scripts/run/run-watcher-guard.ts +5 -3
  347. package/scripts/run/run-watcher-hook.ts +1 -1
  348. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  349. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  350. package/scripts/run/ship/run-ship-lock.ts +50 -0
  351. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  352. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  353. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  354. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  355. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  356. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  357. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  358. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  359. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  360. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +23 -8
  361. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  362. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  363. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  364. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  365. package/scripts/run/wake/run-wake-restart.ts +58 -0
  366. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  367. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  368. package/scripts/safe-chain/preinstall-command.ts +20 -21
  369. package/scripts/safe-chain/project-config.ts +19 -15
  370. package/scripts/sync/sync-ai-files.ts +1 -1
  371. package/scripts/sync/sync-configs.ts +2 -1
  372. package/scripts/sync/sync.ts +15 -2
  373. package/scripts/test/unit-worker-share.ts +1 -0
  374. package/scripts/time-runtime/time-background.ts +5 -1
  375. package/scripts/time-runtime/time-instant.ts +3 -1
  376. package/scripts/time-runtime/time-spans.ts +6 -139
  377. package/scripts/time-runtime/time-tool-call.ts +160 -0
  378. package/scripts/version/effective-upstream.ts +2 -20
  379. package/scripts/version/latest-corepack.ts +17 -17
  380. package/scripts/version/latest-regression.ts +1 -1
  381. package/scripts/version/latest-update.ts +4 -4
  382. package/scripts/version/npm-registry.ts +20 -5
  383. package/scripts/version/publishable-range-check.ts +2 -2
  384. package/scripts/version/publishable-range.ts +1 -1
  385. package/scripts/version/release-age.ts +48 -38
  386. package/scripts/version/release-hold.ts +1 -1
  387. package/scripts/version/safe-chain-version-update.ts +40 -0
  388. package/scripts/version/upgrade-shell-command.ts +48 -8
  389. package/scripts/version/version-check-logic.ts +23 -14
  390. package/scripts/version/version-commands.ts +23 -11
  391. package/scripts/version/version-remote.ts +28 -13
  392. package/templates/pnpm-workspace.basic.yaml +4 -0
  393. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  394. package/templates/workflows/github-release.yml +2 -0
  395. package/scripts/build/build-config-merge.ts +0 -27
  396. package/scripts/build/build-managed-marker.ts +0 -27
  397. package/scripts/build/build-self-sync-guard.ts +0 -27
  398. package/scripts/build/build-version.ts +0 -26
  399. package/scripts/version/preinstall-version-update.ts +0 -86
  400. /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
  401. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  402. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  403. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  404. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  405. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  406. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  407. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  408. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  409. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  410. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  411. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  412. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  413. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  414. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  415. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  416. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  417. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  418. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  419. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  420. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  421. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  422. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  423. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  424. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  425. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -2,6 +2,7 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
4
4
  import { project_profile } from '#scripts/init/project-profile'
5
+ import { ancestor_directories } from '#scripts/lib/ancestor-directories'
5
6
  import { json_value } from '#scripts/lib/json-value'
6
7
  import { SKIP_MARKER } from '#scripts/test/skip-marker'
7
8
  import { z } from 'zod'
@@ -43,15 +44,11 @@ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
43
44
  const object_schema = z.record(z.string(), z.unknown())
44
45
 
45
46
  function project_root(directory: string): string {
46
- let current = directory
47
-
48
- while (!existsSync(path.join(current, PACKAGE_JSON))) {
49
- const parent = path.dirname(current)
50
- if (parent === current) return directory
51
- current = parent
52
- }
53
-
54
- return current
47
+ return (
48
+ ancestor_directories.nearest(directory, (candidate) =>
49
+ existsSync(path.join(candidate, PACKAGE_JSON)),
50
+ ) ?? directory
51
+ )
55
52
  }
56
53
 
57
54
  function is_ignored(entry: string): boolean {
@@ -1,4 +1,5 @@
1
1
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
2
+ import { COMMAND_TIMEOUT_MS } from '#scripts/lib/timeouts'
2
3
  import { execa } from 'execa'
3
4
 
4
5
  // joshuafolkken/kit#934: `josh gate`'s type-check step used to be `tsc --noEmit` for every project.
@@ -28,8 +29,6 @@ const TOOLKIT_BINS: ReadonlyArray<string> = ['josh-app', 'josh-game']
28
29
  // a bracketed warning printed first would otherwise be read as the command list.
29
30
  const USAGE_COMMANDS_PATTERN = /Usage:[^<>\n]*<([^<>\n]+)>/u
30
31
  const COMMAND_SEPARATOR = '|'
31
- // A toolkit that prints its usage answers in milliseconds; anything slower is hung.
32
- const USAGE_TIMEOUT_MS = 30_000
33
32
 
34
33
  function parse_usage_commands(output: string): ReadonlyArray<string> {
35
34
  const group = USAGE_COMMANDS_PATTERN.exec(output)?.[1]
@@ -46,7 +45,7 @@ async function read_toolkit_commands(bin_path: string): Promise<ReadonlyArray<st
46
45
  const result = await execa(bin_path, [], {
47
46
  all: true,
48
47
  reject: false,
49
- timeout: USAGE_TIMEOUT_MS,
48
+ timeout: COMMAND_TIMEOUT_MS,
50
49
  })
51
50
 
52
51
  return parse_usage_commands(result.all)
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
5
5
  import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
6
6
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
7
+ import { lane_ledger } from '#scripts/lane/lane-ledger'
7
8
  import { bounded_pool } from '#scripts/lib/bounded-pool'
8
9
  import { buffered_process, FAIL_EXIT_CODE } from '#scripts/lib/buffered-process'
9
10
  import { review_stamps } from '#scripts/review/review-stamps'
@@ -166,7 +167,7 @@ interface BudgetContext {
166
167
  function default_budget(): BudgetContext {
167
168
  return {
168
169
  available_cores: availableParallelism(),
169
- is_reserved: !unit_worker_share.is_nested_run(),
170
+ is_reserved: !unit_worker_share.is_nested_run() && !core_budget.is_held(),
170
171
  }
171
172
  }
172
173
 
@@ -222,10 +223,17 @@ async function run_gate_steps(
222
223
  ): Promise<ReadonlyArray<GateStepResult>> {
223
224
  const reserved = await build_reserved_steps(process.cwd(), plan, budget.available_cores)
224
225
 
225
- return await bounded_pool.bounded_map(
226
- reserved,
227
- plan.concurrency,
228
- async (entry) => await run_reserved_step(entry, budget),
226
+ // **The checks run under the held mark, so a check's own dispatch claims nothing**
227
+ // (joshuafolkken/kit#3345). Each check is a `pnpm josh <target>` child, and a target that declares
228
+ // a weight would otherwise claim a second place for the cores reserved for it here. A nested gate
229
+ // sets the mark too, since its checks are covered by the outer gate's reservation.
230
+ return await core_budget.with_held_mark(
231
+ async () =>
232
+ await bounded_pool.bounded_map(
233
+ reserved,
234
+ plan.concurrency,
235
+ async (entry) => await run_reserved_step(entry, budget),
236
+ ),
229
237
  )
230
238
  }
231
239
 
@@ -299,6 +307,9 @@ interface GateOptions {
299
307
  // The scoped records' paths, overridable so a test can plant a green pair rather than depend on the
300
308
  // surrounding checkout's — the reason `scoped-green.ts` takes a `source` per stamp.
301
309
  scoped_sources?: ScopedSources
310
+ // The lane ledger a finished gate's duration is appended to (joshuafolkken/kit#3355). Only the CLI
311
+ // entry resolves it, so a suite driving the gate never writes a measurement of checks that never ran.
312
+ ledger_path?: string | undefined
302
313
  }
303
314
 
304
315
  // **The gate refuses to start when the scoped pair has not been green on this tree**
@@ -339,14 +350,20 @@ async function settle_gate(
339
350
  tree: GateTree,
340
351
  options: GateOptions & { started_at: number },
341
352
  ): Promise<number> {
353
+ const elapsed_ms = performance.now() - options.started_at
342
354
  const failed_labels = gate_report.report_gate_steps(results, {
343
355
  is_verbose: options.is_verbose ?? false,
344
356
  log_path: options.log_path,
345
- elapsed_ms: performance.now() - options.started_at,
357
+ elapsed_ms,
346
358
  step_count: String(plan.checks.length),
347
359
  })
360
+ const is_passed = failed_labels.length === 0
348
361
 
349
- if (failed_labels.length > 0) return FAIL_EXIT_CODE
362
+ if (options.ledger_path !== undefined) {
363
+ await lane_ledger.record_gate(options.ledger_path, elapsed_ms, is_passed)
364
+ }
365
+
366
+ if (!is_passed) return FAIL_EXIT_CODE
350
367
 
351
368
  await record_whole_gate(plan, results, tree, options)
352
369
 
@@ -474,7 +491,9 @@ async function run_gate_command(
474
491
  // summary, written last, is the first thing lost. Setting the code lets the writes drain and the
475
492
  // process end on its own, which it can, since every child has already exited by here.
476
493
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
477
- process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX))
494
+ process.exitCode = await run_gate_command(process.argv.slice(FIRST_ARGUMENT_INDEX), {
495
+ ledger_path: await lane_ledger.target(),
496
+ })
478
497
  }
479
498
 
480
499
  const verification_gate = {
@@ -14,8 +14,8 @@ const FULL_PAGE_QUERY = '?per_page=100'
14
14
  // `gh api` expands `{owner}` and `{repo}` from the current repository, so the unqualified form needs
15
15
  // no extra lookup to name a path. An explicit `repo` names another repository — the form a
16
16
  // cross-repository reference takes.
17
- function repo_api_path(repo?: string): string {
18
- return `repos/${repo ?? '{owner}/{repo}'}`
17
+ function repo_api_path(repo = '{owner}/{repo}'): string {
18
+ return `repos/${repo}`
19
19
  }
20
20
 
21
21
  // The issue collection — what a listing pages through and what a creation posts to. One segment,
@@ -1,8 +1,8 @@
1
+ import { blocking_issue_schema } from '#scripts/git/git-schemas'
1
2
  import {
2
3
  parse_json_array_or_undefined,
3
4
  parse_json_object_safe,
4
5
  } from '#scripts/git/parse-json-array'
5
- import { blocking_issue_schema } from '#scripts/git/schemas'
6
6
  import type { IssueReference } from '#scripts/issue/issue-reference'
7
7
  import { z } from 'zod'
8
8
  import { MERGED_STATE, to_gh_state } from './git-gh-rest-state'
@@ -163,8 +163,8 @@ function empty_blocked_by(): BlockedBy {
163
163
  // repository and names a different issue there (joshuafolkken/kit#1126). Undefined when the field is
164
164
  // absent or shaped otherwise; the caller then falls back to the repository it is reading in, which is
165
165
  // what an unqualified relation has always meant.
166
- function repo_of_url(repository_url: string | undefined): string | undefined {
167
- const match = REPOSITORY_URL.exec(repository_url ?? '')
166
+ function repo_of_url(repository_url: string | undefined = ''): string | undefined {
167
+ const match = REPOSITORY_URL.exec(repository_url)
168
168
 
169
169
  return match?.[REPOSITORY_URL_GROUP]
170
170
  }
@@ -29,6 +29,7 @@ const DELETE_METHOD = 'DELETE'
29
29
  // object, so the same value is unwrapped out of it rather than the shape being changed.
30
30
  const HTML_URL_FILTER = '.html_url'
31
31
  const ID_FILTER = '.id'
32
+ const LABEL_NAMES_FILTER = '.[].name'
32
33
 
33
34
  const LABELS_SEGMENT = '/labels'
34
35
  const COMMENTS_SEGMENT = '/comments'
@@ -182,17 +183,60 @@ async function issue_create_with_label(input: {
182
183
  )
183
184
  }
184
185
 
186
+ // The outcome of applying one label, with the swallowed failure's gist kept for a caller that has to
187
+ // say why it refused (joshuafolkken/kit#3312).
188
+ type LabelWrite = { is_applied: true } | { is_applied: false; reason: string }
189
+
190
+ // The issue's label names, read back after a failed write. A read that itself fails answers
191
+ // `undefined`, which the caller treats as "not applied" — the write's own error is the reason then.
192
+ async function read_label_names(issue_number: string): Promise<Array<string> | undefined> {
193
+ try {
194
+ const raw = await git_gh_exec.exec_gh_api({
195
+ path: `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`,
196
+ should_paginate: true,
197
+ jq_filter: LABEL_NAMES_FILTER,
198
+ })
199
+
200
+ return raw.split('\n').map((name) => name.trim())
201
+ } catch (error) {
202
+ error_text.trace_swallowed('git_gh_issue_write.read_label_names', error)
203
+
204
+ return undefined
205
+ }
206
+ }
207
+
208
+ // **A failed answer is not a failed write.** The POST is idempotent, and a timeout, a non-zero exit
209
+ // after the write or a secondary rate limit can each throw after the label landed — which refused a
210
+ // lane dispatch whose label was in fact applied (joshuafolkken/kit#3312). So a throw is confirmed by
211
+ // reading the labels back, and only a label that is really absent answers `is_applied: false`.
212
+ async function issue_apply_label(issue_number: string, label: string): Promise<LabelWrite> {
213
+ const labels_path = `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`
214
+
215
+ try {
216
+ await git_gh_exec.exec_gh_api({ path: labels_path, body: JSON.stringify({ labels: [label] }) })
217
+
218
+ return { is_applied: true }
219
+ } catch (error) {
220
+ const names = await read_label_names(issue_number)
221
+
222
+ if (names?.includes(label) === true) return { is_applied: true }
223
+ error_text.trace_swallowed('git_gh_issue_write.issue_apply_label', error)
224
+
225
+ // The first line is the gist: gh follows it with the response body, which a one-line refusal
226
+ // does not need.
227
+ const [gist = ''] = error_text.message_of(error).split('\n', 1)
228
+
229
+ return { is_applied: false, reason: gist }
230
+ }
231
+ }
232
+
185
233
  // Applied after the body edit so a failure leaves an issue with the epic sections and no label,
186
234
  // which `epic:check` reports — rather than a labelled issue with nothing to track. The caller checks
187
235
  // the return: the label is what the auto-close filters on (joshuafolkken/kit#865).
188
236
  async function issue_add_label(issue_number: string, label: string): Promise<boolean> {
189
- return await did_write_succeed(
190
- async () =>
191
- await git_gh_exec.exec_gh_api({
192
- path: `${git_gh_api_path.issue_api_path(issue_number)}${LABELS_SEGMENT}`,
193
- body: JSON.stringify({ labels: [label] }),
194
- }),
195
- )
237
+ const write = await issue_apply_label(issue_number, label)
238
+
239
+ return write.is_applied
196
240
  }
197
241
 
198
242
  // The counterpart to the addition above, for a run taking its own marker back off
@@ -279,6 +323,7 @@ const git_gh_issue_write = {
279
323
  label_ensure,
280
324
  issue_create_request,
281
325
  issue_create_with_label,
326
+ issue_apply_label,
282
327
  issue_add_label,
283
328
  issue_remove_label,
284
329
  issue_add_blocked_by,
@@ -286,3 +331,4 @@ const git_gh_issue_write = {
286
331
  }
287
332
 
288
333
  export { git_gh_issue_write }
334
+ export type { LabelWrite }
@@ -89,9 +89,24 @@ interface PullMergeState {
89
89
  type PullNumberRead =
90
90
  { kind: 'read'; pr_number: number } | { kind: 'missing' } | { kind: 'unreadable'; cause: unknown }
91
91
 
92
+ // The branch-keyed detail read, in the same three answers. `missing` and `unreadable` are the
93
+ // resolution's own; `unreadable` also covers a resolved number whose detail read failed, which every
94
+ // branch-keyed reader used to fold into "no pull request" (joshuafolkken/kit#3263).
95
+ type PullRead = Exclude<PullNumberRead, { kind: 'read' }> | { kind: 'read'; pull: RestPull }
96
+
92
97
  // The lookups still in flight, keyed the same way the resolved numbers are.
93
98
  const pending_pr_number_by_branch = new Map<string, Promise<PullNumberRead>>()
94
99
 
100
+ // The detail reads, keyed by number and remembered for the life of the command
101
+ // (joshuafolkken/kit#3263). `josh followup` asks `pr_get_body` and `pr_get_url` in one tick — the
102
+ // same `GET /pulls/{N}` each time. The promise is stored, so readers in one tick share the request as
103
+ // well as its answer; a read that fails is dropped and re-tried, the rule the number memo holds.
104
+ // **Every write to a pull request clears it** through `forget_pr_numbers`, and the two state readers
105
+ // never read it: `pr_get_merge_state` answers where the pull request stands *now*, which `josh ship`
106
+ // asks again after every merge it pushes, and `pr_view` decides whether `josh git -y` writes onto the
107
+ // pull request — after a commit and a push its preflight read would otherwise outlast.
108
+ const pull_by_number = new Map<number, Promise<RestPull>>()
109
+
95
110
  async function fetch_pr_number_read(branch_name: string): Promise<PullNumberRead> {
96
111
  try {
97
112
  const pr_number = await fetch_pr_number(branch_name)
@@ -130,10 +145,11 @@ async function start_pr_number_read(branch_name: string): Promise<PullNumberRead
130
145
  }
131
146
  }
132
147
 
133
- // Defined after the map it clears, so the two memos are declared before anything reaches them.
148
+ // Defined after the maps it clears, so the memos are declared before anything reaches them.
134
149
  function forget_pr_numbers(): void {
135
150
  pr_number_by_branch.clear()
136
151
  pending_pr_number_by_branch.clear()
152
+ pull_by_number.clear()
137
153
  }
138
154
 
139
155
  async function read_pr_number(branch_name: string): Promise<PullNumberRead> {
@@ -144,9 +160,9 @@ async function read_pr_number(branch_name: string): Promise<PullNumberRead> {
144
160
  }
145
161
 
146
162
  // `undefined` covers both "this branch has no pull request" and "the lookup failed", which is the
147
- // distinction `gh pr view` never made either — every *read* here folds them together on purpose:
148
- // `pr_view` answers the empty string either way, the comment readers `undefined`. The two callers
149
- // that act on the absence rather than displaying it — `require_pr_number` and `pr_exists` — opt out.
163
+ // distinction `gh pr view` never made either — the *display* reads here fold them together on
164
+ // purpose: the comment readers answer `undefined` either way. The callers that act on the absence
165
+ // rather than displaying it — `require_pr_number`, `pr_exists` and `pr_view` — opt out.
150
166
  async function resolve_pr_number(branch_name: string): Promise<number | undefined> {
151
167
  const read = await read_pr_number(branch_name)
152
168
 
@@ -186,19 +202,43 @@ async function read_pull(pr_number: number): Promise<RestPull> {
186
202
  return git_gh_pr_rest.parse_rest_pull(json)
187
203
  }
188
204
 
205
+ // The identity check keeps a failure from evicting a read that `forget_pr_numbers` already replaced.
206
+ async function read_remembered_pull(pr_number: number): Promise<RestPull> {
207
+ const pending = pull_by_number.get(pr_number) ?? read_pull(pr_number)
208
+
209
+ pull_by_number.set(pr_number, pending)
210
+
211
+ try {
212
+ return await pending
213
+ } catch (error) {
214
+ if (pull_by_number.get(pr_number) === pending) pull_by_number.delete(pr_number)
215
+ throw error
216
+ }
217
+ }
218
+
189
219
  // One pull request named by its branch: the resolution, then the detail read that carries
190
220
  // `mergeable` and `mergeable_state`, which the lookup listing does not.
191
- async function read_pull_of_branch(branch_name: string): Promise<RestPull | undefined> {
192
- const pr_number = await resolve_pr_number(branch_name)
193
- if (pr_number === undefined) return undefined
221
+ async function read_pull_of_branch(
222
+ branch_name: string,
223
+ read: (pr_number: number) => Promise<RestPull> = read_remembered_pull,
224
+ ): Promise<PullRead> {
225
+ const number_read = await read_pr_number(branch_name)
226
+ if (number_read.kind !== 'read') return number_read
194
227
 
195
228
  try {
196
- return await read_pull(pr_number)
197
- } catch {
198
- return undefined
229
+ return { kind: 'read', pull: await read(number_read.pr_number) }
230
+ } catch (error) {
231
+ return { kind: 'unreadable', cause: error }
199
232
  }
200
233
  }
201
234
 
235
+ // The display readers' fold: "no pull request" and "nothing could be read" are one empty answer.
236
+ async function read_pull_or_undefined(branch_name: string): Promise<RestPull | undefined> {
237
+ const read = await read_pull_of_branch(branch_name)
238
+
239
+ return read.kind === 'read' ? read.pull : undefined
240
+ }
241
+
202
242
  // **`false` means the branch has no pull request, and nothing else.** This is the second half of
203
243
  // joshuafolkken/kit#1048's distinction, on the caller that needs it most: `pr_exists` is not read for
204
244
  // display, it decides whether `git-pr.ts` **opens** a pull request. Folding an unreadable lookup into
@@ -218,7 +258,7 @@ async function pr_get_number(branch_name: string): Promise<number | undefined> {
218
258
  }
219
259
 
220
260
  async function pr_get_url(branch_name: string): Promise<string | undefined> {
221
- const pull = await read_pull_of_branch(branch_name)
261
+ const pull = await read_pull_or_undefined(branch_name)
222
262
  if (pull?.html_url === undefined) return undefined
223
263
 
224
264
  return git_gh_helpers.parse_pr_state_string(pull.html_url)
@@ -229,9 +269,11 @@ async function pr_get_url(branch_name: string): Promise<string | undefined> {
229
269
  // made on GitHub moves without touching the local branch. `undefined` covers both "no pull request"
230
270
  // and "nothing could be read": `followup` then takes its ordinary path, whose merge step settles a
231
271
  // pull request that did merge after all (`pr_merge`), so an unread answer costs a wait, not a wrong tail.
272
+ // Read past the detail memo: `josh ship` asks it again after each merge it pushes (joshuafolkken/kit#3263).
232
273
  async function pr_get_merge_state(branch_name: string): Promise<PullMergeState | undefined> {
233
- const pull = await read_pull_of_branch(branch_name)
234
- if (pull === undefined) return undefined
274
+ const read = await read_pull_of_branch(branch_name, read_pull)
275
+ if (read.kind !== 'read') return undefined
276
+ const { pull } = read
235
277
 
236
278
  return {
237
279
  is_merged: git_gh_pr_rest.is_merged(pull),
@@ -244,19 +286,22 @@ async function pr_get_merge_state(branch_name: string): Promise<PullMergeState |
244
286
  // REST answers JSON null for a pull request with no body where `gh --json body` answered an empty
245
287
  // string, and both used to arrive here as the empty answer this folds to `undefined`.
246
288
  async function pr_get_body(branch_name: string): Promise<string | undefined> {
247
- const pull = await read_pull_of_branch(branch_name)
289
+ const pull = await read_pull_or_undefined(branch_name)
248
290
  const body = pull?.body
249
291
 
250
292
  return typeof body === 'string' && body.length > 0 ? body : undefined
251
293
  }
252
294
 
253
- // The empty string means "there is nothing to read" — a branch with no pull request, or a read that
254
- // failed. `git-pr.ts` checks the length before parsing.
295
+ // The empty string means "this branch has no pull request", and nothing else. A read that failed
296
+ // throws, carrying gh's reason as the `cause`: `git-pr.ts` decides from this state whether the
297
+ // branch's pull request merged, and a rate limit answered as "no state" sent a merged pull request
298
+ // down the open one's path (joshuafolkken/kit#3263). Read past the detail memo, like the merge state.
255
299
  async function pr_view(branch_name: string): Promise<string> {
256
- const pull = await read_pull_of_branch(branch_name)
257
- if (pull === undefined) return ''
300
+ const read = await read_pull_of_branch(branch_name, read_pull)
301
+ if (read.kind === 'unreadable') throw to_unreadable_error(branch_name, read.cause)
302
+ if (read.kind === 'missing') return ''
258
303
 
259
- return JSON.stringify(git_gh_pr_rest.to_pr_info(pull))
304
+ return JSON.stringify(git_gh_pr_rest.to_pr_info(read.pull))
260
305
  }
261
306
 
262
307
  // `undefined` when the listing could not be read — a failed request, or a PR whose number could not
@@ -1,8 +1,8 @@
1
+ import { rest_comment_schema } from '#scripts/git/git-schemas'
1
2
  import {
2
3
  parse_json_array_or_undefined,
3
4
  parse_json_object_safe,
4
5
  } from '#scripts/git/parse-json-array'
5
- import { rest_comment_schema } from '#scripts/git/schemas'
6
6
  import { z } from 'zod'
7
7
  import { MERGED_STATE, to_gh_state } from './git-gh-rest-state'
8
8
 
@@ -152,10 +152,12 @@ async function pr_comment(branch_name: string, body: string): Promise<string> {
152
152
 
153
153
  // Replaces the body of the branch's open pull request. `git -y` opens a pull request once, so a
154
154
  // body supplied on a later run — the live-execution evidence `followup` gates the merge on
155
- // (joshuafolkken/kit#2446) — reaches an already-open one only through this write.
155
+ // (joshuafolkken/kit#2446) — reaches an already-open one only through this write. The remembered
156
+ // detail read holds the old body, so it is cleared first (joshuafolkken/kit#3263).
156
157
  async function pr_update_body(branch_name: string, body: string): Promise<void> {
157
158
  const pr_number = await require_pr_number(branch_name)
158
159
 
160
+ forget_pr_numbers()
159
161
  await git_gh_exec.exec_gh_api({
160
162
  path: git_gh_api_path.pull_api_path(String(pr_number)),
161
163
  method: PATCH_METHOD,
@@ -163,7 +165,9 @@ async function pr_update_body(branch_name: string, body: string): Promise<void>
163
165
  })
164
166
  }
165
167
 
168
+ // A merge moves the remembered detail read's state, so it is cleared first (joshuafolkken/kit#3263).
166
169
  async function put_merge(pr_number: number): Promise<void> {
170
+ forget_pr_numbers()
167
171
  await git_gh_exec.exec_gh_api({
168
172
  path: git_gh_api_path.pull_merge_api_path(String(pr_number)),
169
173
  method: PUT_METHOD,
@@ -1,6 +1,6 @@
1
1
  // cspell:words coderabbit coderabbitai
2
+ import { ai_review_pull_comment_schema } from '#scripts/git/git-schemas'
2
3
  import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
3
- import { ai_review_pull_comment_schema } from '#scripts/git/schemas'
4
4
  import {
5
5
  classify_ai_review_comments,
6
6
  type ClassifiedFinding,
@@ -191,9 +191,7 @@ function evaluate_pr_state(snapshot: PrStateSnapshot): PrEvaluation {
191
191
 
192
192
  if (failure !== undefined) return failure
193
193
 
194
- if (is_mergeable_state(snapshot) && is_every_required_passing(statuses)) {
195
- return 'success'
196
- }
194
+ if (is_mergeable_state(snapshot) && is_every_required_passing(statuses)) return 'success'
197
195
 
198
196
  return 'pending'
199
197
  }
@@ -1,4 +1,4 @@
1
- import { pr_raw_schema, type RollupItemData } from '#scripts/git/schemas'
1
+ import { pr_raw_schema, type RollupItemData } from '#scripts/git/git-schemas'
2
2
  import { json_value } from '#scripts/lib/json-value'
3
3
 
4
4
  const CHECK_STATUS_PASS = 'pass'
@@ -7,7 +7,6 @@ import {
7
7
  SECONDS_TO_MS,
8
8
  wait_for_pr_success,
9
9
  type PrStateEvaluator,
10
- type ReviewDecisionPredicate,
11
10
  } from './git-pr-checks'
12
11
  import { is_coderabbit_check, is_required_check, type PrEvaluation } from './git-pr-checks-eval'
13
12
  import {
@@ -116,12 +115,14 @@ function describe_checks_failure(snapshot: PrStateSnapshot): string {
116
115
  // **The watch never reads `review_decision`**, so the poll must not spend a request fetching it. It
117
116
  // is the same difference `evaluate_checks_settled` is written for, said to the fetcher instead of to
118
117
  // the verdict: three requests a poll rather than four (joshuafolkken/kit#1043).
119
- const SHOULD_NEVER_READ_REVIEW_DECISION: ReviewDecisionPredicate = () => false
118
+ function should_never_read_review_decision(): boolean {
119
+ return false
120
+ }
120
121
 
121
122
  const CHECKS_SETTLED_EVALUATOR: PrStateEvaluator = {
122
123
  evaluate: evaluate_checks_settled,
123
124
  describe: describe_checks_failure,
124
- should_read_review_decision: SHOULD_NEVER_READ_REVIEW_DECISION,
125
+ should_read_review_decision: should_never_read_review_decision,
125
126
  }
126
127
 
127
128
  async function watch_until_settled(branch_name: string): Promise<void> {
@@ -145,7 +146,7 @@ async function watch_until_settled(branch_name: string): Promise<void> {
145
146
  // from failing on the first read: two minutes is the grace, and an empty rollup at the end of them
146
147
  // is the answer `gh` gave immediately.
147
148
  async function fail_when_no_checks(branch_name: string): Promise<void> {
148
- const snapshot = await default_fetch_pr_state(branch_name, SHOULD_NEVER_READ_REVIEW_DECISION)
149
+ const snapshot = await default_fetch_pr_state(branch_name, should_never_read_review_decision)
149
150
  if (snapshot.rollup.length === 0) throw new Error(NO_CHECKS_MESSAGE)
150
151
  }
151
152
 
@@ -173,7 +174,7 @@ export {
173
174
  git_pr_checks_watch,
174
175
  fail_when_no_checks,
175
176
  CHECKS_SETTLED_EVALUATOR,
176
- SHOULD_NEVER_READ_REVIEW_DECISION,
177
+ should_never_read_review_decision,
177
178
  evaluate_checks_settled,
178
179
  is_awaited_check,
179
180
  describe_checks_failure,
@@ -1,4 +1,4 @@
1
- import { package_name_schema } from '#scripts/git/schemas'
1
+ import { package_name_schema } from '#scripts/git/git-schemas'
2
2
  import { poll } from '#scripts/lib/poll'
3
3
  import { git_gh_pr_auto_merge } from './git-gh-pr-auto-merge'
4
4
  import { git_gh_pr_snapshot } from './git-gh-pr-snapshot'
@@ -87,7 +87,9 @@ type ReviewDecisionPredicate = (snapshot: PrStateSnapshot) => boolean
87
87
 
88
88
  // The conservative answer, and the one every evaluator that does not say gets: read it every poll,
89
89
  // which is what all of them did before the predicate existed.
90
- const SHOULD_ALWAYS_READ_REVIEW_DECISION: ReviewDecisionPredicate = () => true
90
+ function should_always_read_review_decision(): boolean {
91
+ return true
92
+ }
91
93
 
92
94
  type PrStateFetcher = (
93
95
  branch_name: string,
@@ -199,7 +201,7 @@ async function read_pr_state(
199
201
  try {
200
202
  const snapshot = await input.options.fetcher(
201
203
  input.options.branch_name,
202
- evaluator.should_read_review_decision ?? SHOULD_ALWAYS_READ_REVIEW_DECISION,
204
+ evaluator.should_read_review_decision ?? should_always_read_review_decision,
203
205
  )
204
206
 
205
207
  return { kind: 'read', snapshot }
@@ -297,7 +299,7 @@ async function wait_for_pr_success(options: WaitForPrSuccessOptions): Promise<Pr
297
299
  // (joshuafolkken/kit#1043).
298
300
  async function default_fetch_pr_state(
299
301
  branch_name: string,
300
- should_read_review_decision: ReviewDecisionPredicate = SHOULD_ALWAYS_READ_REVIEW_DECISION,
302
+ should_read_review_decision: ReviewDecisionPredicate = should_always_read_review_decision,
301
303
  ): Promise<PrStateSnapshot> {
302
304
  const checks = await git_gh_pr_snapshot.pr_get_checks_snapshot(branch_name)
303
305
  const snapshot = parse_pr_state_snapshot(checks.snapshot_json)
@@ -385,7 +387,7 @@ export {
385
387
  is_pr_checks_timeout,
386
388
  default_fetch_pr_state,
387
389
  MERGE_GATE_EVALUATOR,
388
- SHOULD_ALWAYS_READ_REVIEW_DECISION,
390
+ should_always_read_review_decision,
389
391
  }
390
392
  export type { MergeProgress, PrStateFetcher, PrStateEvaluator, ReviewDecisionPredicate }
391
393
  export {
@@ -1,5 +1,5 @@
1
+ import { pull_comment_schema } from '#scripts/git/git-schemas'
1
2
  import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
2
- import { pull_comment_schema } from '#scripts/git/schemas'
3
3
  import { git_gh_command } from './git-gh-command'
4
4
  import { has_ignore_reason } from './git-pr-ai-review'
5
5
 
@@ -8,9 +8,7 @@ function has_error_cause(error: Error): boolean {
8
8
 
9
9
  function is_pr_already_exists_error(error: unknown): boolean {
10
10
  if (error instanceof Error) {
11
- if (is_pr_already_exists_message(error.message)) {
12
- return true
13
- }
11
+ if (is_pr_already_exists_message(error.message)) return true
14
12
 
15
13
  return has_error_cause(error)
16
14
  }
@@ -1,6 +1,6 @@
1
1
  import { pr_classification, type ReleaseClassification } from '#scripts/ci/pr-classification'
2
2
  import type { IssueInfo } from '#scripts/git/git-issue'
3
- import { pr_info_schema } from '#scripts/git/schemas'
3
+ import { pr_info_schema } from '#scripts/git/git-schemas'
4
4
  import { animation_helpers, type AnimationOptions } from '#scripts/lib/animation-helpers'
5
5
  import { git_gh_command } from './git-gh-command'
6
6
  import { git_pr_error } from './git-pr-error'
@@ -98,18 +98,14 @@ function parse_pr_state(pr_info_json: string): string | undefined {
98
98
  }
99
99
  }
100
100
 
101
- async function get_pr_state_safe(branch_name: string): Promise<string | undefined> {
102
- try {
103
- const pr_info_json = await git_gh_command.pr_view(branch_name)
101
+ // A read that failed throws rather than answering `undefined`: "no state" is read as "not merged",
102
+ // which would write onto a pull request that already merged (joshuafolkken/kit#3263).
103
+ async function get_pr_state(branch_name: string): Promise<string | undefined> {
104
+ const pr_info_json = await git_gh_command.pr_view(branch_name)
104
105
 
105
- if (pr_info_json.length === 0) {
106
- return undefined
107
- }
106
+ if (pr_info_json.length === 0) return undefined
108
107
 
109
- return parse_pr_state(pr_info_json)
110
- } catch {
111
- return undefined
112
- }
108
+ return parse_pr_state(pr_info_json)
113
109
  }
114
110
 
115
111
  function is_pr_state_merged(pr_state: string | undefined): boolean {
@@ -137,7 +133,7 @@ async function handle_existing_pr(
137
133
  branch_name: string,
138
134
  options: { label: ReleaseClassification; should_replace_body: boolean },
139
135
  ): Promise<void> {
140
- const pr_state_result = await get_pr_state_safe(branch_name)
136
+ const pr_state_result = await get_pr_state(branch_name)
141
137
 
142
138
  if (is_pr_state_merged(pr_state_result)) {
143
139
  git_pr_messages.display_merged_pr_message()
@@ -181,7 +177,7 @@ function build_body(issue_info: IssueInfo, extra_body?: string): string {
181
177
 
182
178
  async function existing_pr_label(branch_name: string): Promise<ReleaseClassification | undefined> {
183
179
  if (!(await git_gh_command.pr_exists(branch_name))) return undefined
184
- if (is_pr_state_merged(await get_pr_state_safe(branch_name))) return undefined
180
+ if (is_pr_state_merged(await get_pr_state(branch_name))) return undefined
185
181
 
186
182
  return await git_gh_command.pr_get_classification(branch_name)
187
183
  }
@@ -75,8 +75,8 @@ function build_target(match: RegExpExecArray): ParsedUrl {
75
75
  }
76
76
  }
77
77
 
78
- function parse_with(pattern: RegExp, url: string | undefined): ParsedUrl | undefined {
79
- const match = pattern.exec(url ?? '')
78
+ function parse_with(pattern: RegExp, url: string | undefined = ''): ParsedUrl | undefined {
79
+ const match = pattern.exec(url)
80
80
  if (match === null) return undefined
81
81
 
82
82
  return build_target(match)