@joshuafolkken/kit 1.1178.0 → 1.1240.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 (442) 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 +7 -3
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +9 -4
  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 +10 -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 +2 -1
  20. package/.claude/skills/workflow-commands/latest-gate.md +1 -1
  21. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  22. package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
  23. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  24. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  25. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  26. package/.codex/hooks.json +2 -2
  27. package/.cursorrules +6 -41
  28. package/.github/actions/setup-node/action.yml +1 -1
  29. package/.github/workflows/auto-tag.yml +3 -4
  30. package/.github/workflows/ci.yml +32 -31
  31. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  32. package/.github/workflows/publish.yml +50 -4
  33. package/.github/workflows/sonar-qube.yml +3 -0
  34. package/.pnpmfile.mjs +4 -3
  35. package/.prettierignore +8 -2
  36. package/CLAUDE.md +18 -20
  37. package/README.md +44 -38
  38. package/SECURITY.md +15 -1
  39. package/dist/CLAUDE.md +18 -20
  40. package/dist/commands/document-section-cli.js +734 -0
  41. package/dist/hooks/audit-provision.js +346 -0
  42. package/dist/hooks/batch-guard.js +5 -4
  43. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-4TZVB44A.js} +496 -378
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  46. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  47. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-F3ELQ2AF.js} +3 -3
  48. package/dist/hooks/{chunk-I4XSL4TT.js → chunk-GPTFMK5E.js} +1 -1
  49. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  50. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-KHGB5S3Y.js} +7 -6
  51. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-KHX4U3BG.js} +735 -683
  52. package/dist/hooks/{chunk-VESKRQOF.js → chunk-KRBERBBI.js} +3 -3
  53. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-QVSWU24O.js} +4 -4
  54. package/dist/hooks/{chunk-S6X6WYKR.js → chunk-WK2XSHSI.js} +1643 -923
  55. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-XZYKEM6J.js} +25 -10
  56. package/dist/hooks/{chunk-32GQTL4F.js → chunk-ZQGD4GBY.js} +4 -2
  57. package/dist/hooks/codex-hook-adapter.js +14 -12
  58. package/dist/hooks/duplicate-read-guard.js +6 -5
  59. package/dist/hooks/format-edited.js +4 -3
  60. package/dist/hooks/inputs.json +203 -179
  61. package/dist/hooks/investigation-guard.js +6 -5
  62. package/dist/hooks/pretool-guard.js +12 -10
  63. package/dist/hooks/session-lang.js +2 -4
  64. package/dist/hooks/stop-guard.js +66 -72
  65. package/dist/josh.js +1088 -78
  66. package/dist/version/index.d.ts +7 -2
  67. package/dist/version/index.js +152 -75
  68. package/eslint/base.js +23 -1
  69. package/eslint/rules/code-quality.js +3 -0
  70. package/eslint/rules/early-return-one-liner.js +98 -0
  71. package/eslint/rules/unicorn.js +16 -1
  72. package/package.json +7 -6
  73. package/pnpm-workspace.yaml +9 -4
  74. package/prettier/basic.d.ts +3 -1
  75. package/prettier/index.d.ts +3 -1
  76. package/prettier/static.d.ts +3 -1
  77. package/prompts/coding-standards.md +1 -1
  78. package/prompts/collaboration-workflow/file-edits.md +4 -4
  79. package/prompts/collaboration-workflow/glossary.md +1 -1
  80. package/prompts/collaboration-workflow/issue-template.md +1 -0
  81. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  82. package/prompts/collaboration-workflow/overview.md +2 -2
  83. package/prompts/collaboration-workflow/principles.md +14 -12
  84. package/prompts/collaboration-workflow/report-format.md +1 -1
  85. package/prompts/collaboration-workflow/residency.md +1 -1
  86. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  91. package/prompts/review-rubric.md +1 -2
  92. package/prompts/review.md +1 -1
  93. package/scripts/agent/agent-headless.ts +29 -0
  94. package/scripts/agent/agent-launch-environment.ts +6 -2
  95. package/scripts/agent/codex-agent-argv.ts +2 -1
  96. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  97. package/scripts/backlog/backlog-arrival.ts +3 -3
  98. package/scripts/backlog/backlog-budget.ts +1 -1
  99. package/scripts/backlog/backlog-drive-args.ts +135 -0
  100. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  101. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  102. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  103. package/scripts/backlog/backlog-drive-named.ts +3 -3
  104. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  105. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  106. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  107. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  108. package/scripts/backlog/backlog-drive.ts +1 -1
  109. package/scripts/backlog/backlog-next.ts +16 -5
  110. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  111. package/scripts/backlog/backlog-offer.ts +1 -1
  112. package/scripts/backlog/backlog-plan-cli.ts +1 -1
  113. package/scripts/backlog/backlog-pool.ts +57 -11
  114. package/scripts/backlog/backlog-ready.ts +1 -1
  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/build/build-codex-hooks.ts +3 -1
  119. package/scripts/build/build-commands.ts +66 -0
  120. package/scripts/build/build-hooks.ts +2 -1
  121. package/scripts/build/build-library.ts +12 -3
  122. package/scripts/build/build.ts +70 -0
  123. package/scripts/build/local-bin.ts +3 -13
  124. package/scripts/delegation/investigation-reads.ts +3 -1
  125. package/scripts/discovery/repo-origin.ts +5 -5
  126. package/scripts/doctor/doctor-consumer.ts +2 -2
  127. package/scripts/doctor/doctor-io.ts +2 -3
  128. package/scripts/document/capped-print-part.ts +30 -0
  129. package/scripts/document/capped-print.ts +3 -4
  130. package/scripts/document/document-byte-budget.ts +11 -2
  131. package/scripts/document/document-scan.ts +117 -0
  132. package/scripts/document/document-section.ts +143 -16
  133. package/scripts/document/entry-read-budget.ts +5 -3
  134. package/scripts/document/entry-read-set.ts +10 -89
  135. package/scripts/document/read-set-cost.ts +98 -0
  136. package/scripts/epic/epic-add-body.ts +1 -0
  137. package/scripts/epic/epic-audit-checks.ts +1 -1
  138. package/scripts/epic/epic-audit-rationale.ts +4 -3
  139. package/scripts/epic/epic-bundle-cli.ts +14 -3
  140. package/scripts/epic/epic-bundle-poll.ts +41 -0
  141. package/scripts/epic/epic-busy.ts +1 -1
  142. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  143. package/scripts/epic/epic-classify.ts +1 -1
  144. package/scripts/epic/epic-cli-argv.ts +78 -46
  145. package/scripts/epic/epic-cli-remove.ts +11 -19
  146. package/scripts/epic/epic-cli.ts +74 -73
  147. package/scripts/epic/epic-close-comment.ts +1 -1
  148. package/scripts/epic/epic-close.ts +1 -1
  149. package/scripts/epic/epic-cross-repo.ts +5 -5
  150. package/scripts/epic/epic-graph.ts +1 -1
  151. package/scripts/epic/epic-index.ts +1 -1
  152. package/scripts/epic/epic-issue.ts +2 -6
  153. package/scripts/epic/epic-lane-offer.ts +2 -2
  154. package/scripts/epic/epic-next.ts +29 -42
  155. package/scripts/epic/epic-solo-stale.ts +1 -1
  156. package/scripts/epic/epic-triage.ts +8 -4
  157. package/scripts/epic/epic-validate.ts +1 -1
  158. package/scripts/followup/git-followup-finish.ts +2 -2
  159. package/scripts/followup/git-followup-label.ts +2 -2
  160. package/scripts/followup/git-followup-merged.ts +2 -2
  161. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  162. package/scripts/followup/git-pr-followup.ts +4 -129
  163. package/scripts/gate/core-admission.ts +162 -0
  164. package/scripts/gate/core-budget.ts +190 -68
  165. package/scripts/gate/gate-plan.ts +80 -7
  166. package/scripts/gate/gate-report.ts +28 -9
  167. package/scripts/gate/gate-skip.ts +38 -7
  168. package/scripts/gate/hook-gate-reuse.ts +12 -10
  169. package/scripts/gate/machine-capacity.ts +175 -0
  170. package/scripts/gate/pre-commit-type-check.ts +4 -1
  171. package/scripts/gate/project-checks.ts +6 -9
  172. package/scripts/gate/type-check-step.ts +2 -3
  173. package/scripts/gate/verification-gate.ts +34 -10
  174. package/scripts/gh/git-gh-api-path.ts +2 -2
  175. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  176. package/scripts/gh/git-gh-issue-write.ts +53 -7
  177. package/scripts/gh/git-gh-pr-read.ts +64 -19
  178. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  179. package/scripts/gh/git-gh-pr.ts +5 -1
  180. package/scripts/gh/git-pr-ai-review.ts +1 -1
  181. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  182. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  183. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  184. package/scripts/gh/git-pr-checks.ts +7 -5
  185. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  186. package/scripts/gh/git-pr-error.ts +1 -3
  187. package/scripts/gh/git-pr.ts +9 -13
  188. package/scripts/gh/github-issue-url.ts +2 -2
  189. package/scripts/git/constants.ts +2 -6
  190. package/scripts/git/git-command.ts +20 -218
  191. package/scripts/git/git-diff-reads.ts +225 -0
  192. package/scripts/git/git-error.ts +4 -12
  193. package/scripts/git/git-pre-push-hook.ts +139 -0
  194. package/scripts/git/git-prompt.ts +2 -6
  195. package/scripts/git/git-spawn-sync.ts +3 -1
  196. package/scripts/git/git-spawn.ts +1 -1
  197. package/scripts/git/git-staging.ts +4 -6
  198. package/scripts/git/git-status.ts +3 -9
  199. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  200. package/scripts/hooks/edited-cspell.ts +2 -4
  201. package/scripts/hooks/format-edited-file.ts +3 -12
  202. package/scripts/hooks/pre-push-unit.ts +31 -9
  203. package/scripts/hooks/reserved-run.ts +2 -2
  204. package/scripts/hooks/run-hook.sh +5 -4
  205. package/scripts/hooks/stop-guard.ts +5 -3
  206. package/scripts/init/init-actions.ts +0 -5
  207. package/scripts/init/init-ai-copy.ts +13 -2
  208. package/scripts/init/init-logic-json-merge.ts +19 -0
  209. package/scripts/init/init-logic-secretlint.ts +7 -7
  210. package/scripts/init/init-logic-workspace.ts +41 -2
  211. package/scripts/init/init-logic.ts +16 -26
  212. package/scripts/init/init.ts +10 -16
  213. package/scripts/init/initial-commit.ts +4 -4
  214. package/scripts/init/kit-development-versions.ts +40 -0
  215. package/scripts/init/kit-setup-state.ts +2 -2
  216. package/scripts/init/package-manager-pin.ts +35 -0
  217. package/scripts/init/start-exec.ts +68 -19
  218. package/scripts/init/start-setup-pr.ts +9 -9
  219. package/scripts/init/start-steps.ts +6 -10
  220. package/scripts/issue/defect-rate-cli.ts +1 -1
  221. package/scripts/issue/git-next-issues.ts +1 -1
  222. package/scripts/issue/issue-auto-ok.ts +32 -7
  223. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  224. package/scripts/issue/issue-backlinks.ts +9 -9
  225. package/scripts/issue/issue-comment-cli.ts +19 -13
  226. package/scripts/issue/issue-file-cli.ts +72 -22
  227. package/scripts/issue/issue-file.ts +29 -1
  228. package/scripts/issue/issue-labels.ts +13 -0
  229. package/scripts/issue/issue-release-cli.ts +155 -0
  230. package/scripts/issue/issue-release.ts +58 -0
  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 +14 -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 +18 -0
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-issue.ts +8 -1
  240. package/scripts/josh/josh-commands-lane.ts +24 -1
  241. package/scripts/josh/josh-commands-maintenance.ts +6 -6
  242. package/scripts/josh/josh-commands-run.ts +12 -7
  243. package/scripts/josh/josh-in-process.ts +28 -9
  244. package/scripts/josh/josh-logic.ts +34 -15
  245. package/scripts/josh/process-identity.ts +1 -3
  246. package/scripts/josh/session-language-cli.ts +3 -4
  247. package/scripts/lane/lane-await.ts +5 -1
  248. package/scripts/lane/lane-child-invocation.ts +2 -2
  249. package/scripts/lane/lane-cli.ts +12 -4
  250. package/scripts/lane/lane-dispatch.ts +14 -10
  251. package/scripts/lane/lane-environment.ts +5 -1
  252. package/scripts/lane/lane-handoff.ts +1 -1
  253. package/scripts/lane/lane-install.ts +2 -13
  254. package/scripts/lane/lane-launch-cli.ts +15 -1
  255. package/scripts/lane/lane-ledger.ts +145 -0
  256. package/scripts/lane/lane-leftover.ts +44 -30
  257. package/scripts/lane/lane-load.ts +108 -0
  258. package/scripts/lane/lane-measure-cli.ts +149 -0
  259. package/scripts/lane/lane-occupancy.ts +3 -0
  260. package/scripts/lane/lane-open.ts +5 -5
  261. package/scripts/lane/lane-paths.ts +13 -0
  262. package/scripts/lane/lane-reap.ts +1 -3
  263. package/scripts/lane/lane-registry.ts +11 -0
  264. package/scripts/lane/lane-relaunch.ts +1 -1
  265. package/scripts/lane/lane-stats.ts +224 -0
  266. package/scripts/lane/lane-stray.ts +140 -0
  267. package/scripts/lane/lane-vacant.ts +36 -0
  268. package/scripts/lane/openai-lane-ship.ts +2 -2
  269. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  270. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  271. package/scripts/lane/openai-review-broker.ts +2 -4
  272. package/scripts/lib/ancestor-directories.ts +35 -0
  273. package/scripts/lib/animation-helpers.ts +2 -2
  274. package/scripts/lib/buffered-process.ts +3 -7
  275. package/scripts/lib/cli-flags.ts +71 -1
  276. package/scripts/lib/spawn-exit.ts +2 -2
  277. package/scripts/lib/timeouts.ts +35 -2
  278. package/scripts/lines/line-budget.ts +4 -6
  279. package/scripts/notify/telegram-test.ts +6 -0
  280. package/scripts/observations/observation-ledger-line.ts +6 -6
  281. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  282. package/scripts/observations/observation-ledger.ts +26 -7
  283. package/scripts/overrides/lockfile-overrides.ts +1 -1
  284. package/scripts/overrides/overrides-logic.ts +1 -1
  285. package/scripts/propagate/propagate-git.ts +1 -1
  286. package/scripts/propagate/propagate-publish.ts +2 -2
  287. package/scripts/propagate/propagate-steps.ts +20 -15
  288. package/scripts/propagate/propagate.ts +36 -27
  289. package/scripts/refactor/import-graph.ts +18 -1
  290. package/scripts/refactor/refactor-lint.ts +4 -4
  291. package/scripts/release/github-release-environment.ts +1 -0
  292. package/scripts/release/github-release-runs.ts +115 -0
  293. package/scripts/release/github-release.ts +13 -67
  294. package/scripts/repo/repository-labels.ts +2 -2
  295. package/scripts/repo/required-checks-logic.ts +1 -3
  296. package/scripts/review/live-evidence.ts +2 -4
  297. package/scripts/review/review-brief-cli.ts +1 -1
  298. package/scripts/review/review-finding-ledger.ts +1 -1
  299. package/scripts/review/review-findings-cli.ts +1 -3
  300. package/scripts/rules/decision-oracle-batch.ts +128 -0
  301. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  302. package/scripts/rules/decision-oracle.ts +8 -120
  303. package/scripts/rules/delivered-rules.ts +26 -182
  304. package/scripts/rules/direct-filing.ts +1 -1
  305. package/scripts/rules/early-heartbeat.ts +3 -3
  306. package/scripts/rules/git-argv.ts +1 -1
  307. package/scripts/rules/git-force.ts +1 -1
  308. package/scripts/rules/implementation-cut.ts +2 -2
  309. package/scripts/rules/index-guard.ts +1 -1
  310. package/scripts/rules/issue-comments.ts +118 -0
  311. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  312. package/scripts/rules/lane-background.ts +2 -2
  313. package/scripts/rules/lane-park.ts +18 -0
  314. package/scripts/rules/lane-split-park.ts +128 -0
  315. package/scripts/rules/oracle-firing.ts +1 -1
  316. package/scripts/rules/poll-loop.ts +1 -1
  317. package/scripts/rules/pre-gate-cut.ts +1 -1
  318. package/scripts/rules/protected-files.ts +59 -4
  319. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  320. package/scripts/rules/shell-body-trigger.ts +1 -1
  321. package/scripts/rules/stop-rules.ts +18 -7
  322. package/scripts/rules/test-declared-commit.ts +4 -4
  323. package/scripts/rules/wip-cap.ts +63 -0
  324. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  325. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  326. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  327. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  328. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  329. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +18 -5
  330. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  331. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  332. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  333. package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +1 -0
  334. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  335. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  336. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +20 -2
  337. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  338. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  339. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  340. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  341. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  342. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  343. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  344. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  345. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  346. package/scripts/run/run-ending.ts +2 -2
  347. package/scripts/run/run-halfrun-resume.ts +6 -1
  348. package/scripts/run/run-headless.ts +1 -1
  349. package/scripts/run/run-invocation.ts +85 -84
  350. package/scripts/run/run-liveness.ts +1 -1
  351. package/scripts/run/run-parent-cut-hook.ts +1 -1
  352. package/scripts/run/run-preflight.ts +6 -6
  353. package/scripts/run/run-prep-cli.ts +1 -1
  354. package/scripts/run/run-prrun-resume.ts +1 -1
  355. package/scripts/run/run-report-cli.ts +4 -4
  356. package/scripts/run/run-report.ts +2 -2
  357. package/scripts/run/run-status-cli.ts +1 -1
  358. package/scripts/run/run-step-cli.ts +3 -3
  359. package/scripts/run/run-step.ts +7 -4
  360. package/scripts/run/run-stop-notify.ts +1 -1
  361. package/scripts/run/run-stranded-detect.ts +4 -4
  362. package/scripts/run/run-stranded.ts +2 -2
  363. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  364. package/scripts/run/run-watcher-guard.ts +5 -3
  365. package/scripts/run/run-watcher-hook.ts +1 -1
  366. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  367. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  368. package/scripts/run/ship/run-ship-lock.ts +50 -0
  369. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  370. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  371. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  372. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  373. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  374. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  375. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  376. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  377. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  378. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +25 -11
  379. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +58 -7
  380. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  381. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  382. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +13 -15
  383. package/scripts/run/wake/run-wake-restart.ts +58 -0
  384. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  385. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +16 -65
  386. package/scripts/safe-chain/preinstall-command.ts +20 -21
  387. package/scripts/safe-chain/project-config.ts +19 -15
  388. package/scripts/sync/sync-ai-files.ts +1 -1
  389. package/scripts/sync/sync-configs.ts +2 -1
  390. package/scripts/sync/sync.ts +15 -2
  391. package/scripts/test/unit-worker-share.ts +1 -0
  392. package/scripts/time-runtime/time-background.ts +5 -1
  393. package/scripts/time-runtime/time-instant.ts +3 -1
  394. package/scripts/time-runtime/time-spans.ts +6 -139
  395. package/scripts/time-runtime/time-tool-call.ts +160 -0
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +124 -93
  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 +6 -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/git/{schemas.ts → git-schemas.ts} +0 -0
  419. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  420. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  421. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  422. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  423. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  424. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  425. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  426. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  427. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  428. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  429. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  430. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  431. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  432. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  433. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  434. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  435. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  436. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  437. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  438. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  439. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  440. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  441. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  442. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -1,5 +1,7 @@
1
1
  import { PORCELAIN_FLAG, UNTRACKED_FILES_FLAG } from './constants'
2
+ import { git_diff_reads } from './git-diff-reads'
2
3
  import { create_spawn_error } from './git-execa-error'
4
+ import { git_pre_push_hook } from './git-pre-push-hook'
3
5
  import { git_push_transport } from './git-push-transport'
4
6
  import { git_spawn } from './git-spawn'
5
7
  import { git_spawn_sync } from './git-spawn-sync'
@@ -73,211 +75,6 @@ async function diff_cached(file_path: string): Promise<string> {
73
75
  return await git_spawn.read(['diff', '--cached', file_path])
74
76
  }
75
77
 
76
- const REFS_REMOTES_ORIGIN_PREFIX = 'refs/remotes/origin/'
77
- const NAME_ONLY_FLAG = '--name-only'
78
- const DEFAULT_BRANCH_FALLBACK = 'main'
79
-
80
- async function get_default_branch(): Promise<string> {
81
- try {
82
- const output = await git_spawn.read(['symbolic-ref', 'refs/remotes/origin/HEAD'])
83
- const trimmed = output.trim()
84
-
85
- if (trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) {
86
- return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
87
- }
88
- } catch {
89
- // fall through to default
90
- }
91
-
92
- return DEFAULT_BRANCH_FALLBACK
93
- }
94
-
95
- const MERGE_BASE_COMMAND = 'merge-base'
96
-
97
- // **The ref the default branch is actually at**, which is not the same thing as its name
98
- // (joshuafolkken/kit#1535). `get_default_branch` returns the bare name, and git resolves that to the
99
- // *local* `refs/heads/<default>` — a ref nothing in this workflow advances, because merges happen on
100
- // GitHub and the default branch is checked out in no work tree. This is the single source both the
101
- // lane's start point and `change_base` below resolve through, so a lane cut from
102
- // `refs/remotes/origin/<default>` is measured against that same commit. Measured without it: a lane
103
- // cut from a remote-tracking ref three commits ahead reported those three commits' files as its own.
104
- //
105
- // It falls back to the bare name where there is no remote-tracking ref — a fresh `git init`, a clone
106
- // with no `origin` — which is the reading every caller had before. The one case it reads differently
107
- // is a local default branch *ahead* of the remote, which this workflow does not produce and
108
- // `prevent-main-commit.ts` exists to stop.
109
- async function reference_exists(reference: string): Promise<boolean> {
110
- try {
111
- await git_spawn.read(['rev-parse', '--verify', '--quiet', reference])
112
-
113
- return true
114
- } catch {
115
- return false
116
- }
117
- }
118
-
119
- async function default_branch_reference(): Promise<string> {
120
- const default_branch = await get_default_branch()
121
- const remote_reference = `${REFS_REMOTES_ORIGIN_PREFIX}${default_branch}`
122
-
123
- return (await reference_exists(remote_reference)) ? remote_reference : default_branch
124
- }
125
-
126
- // The commit this branch was cut from — and the base every "changed" reading below measures
127
- // against, in place of the default branch itself.
128
- //
129
- // **A two-dot `git diff <default-branch>` compares a moving *ref* to the working tree, and in a
130
- // linked work tree that ref is shared with every other lane** (joshuafolkken/kit#1527). So when one
131
- // lane merges, an unmerged lane's diff picks that lane's files up in reverse: files this branch
132
- // never touched arrive in `josh review:brief`'s target list, in the digest map `josh gate` stamps,
133
- // and in `josh review:round2`'s fix delta.
134
- //
135
- // **The merge base is the fixed point that removes it**, because it is a commit rather than a ref:
136
- // an advance of the default branch that this branch is an ancestor of does not move it. It is also
137
- // the right answer at both points of a run, and a lane passes through both — with the work still
138
- // uncommitted `HEAD` is the cut commit and the merge base is that same commit, so the diff is the
139
- // uncommitted work; once the branch commits, `HEAD` moves ahead while the merge base stays put, so
140
- // the diff is the branch's whole change plus anything still uncommitted. `<default-branch>...HEAD`
141
- // answers neither, because a three-dot diff ends at `HEAD` and a lane's work is uncommitted for
142
- // most of its run.
143
- //
144
- // **It never reports less than the old reading did.** The new set is the branch's own change; the
145
- // old one was that same set plus the inverse of whatever the default branch had advanced by — so
146
- // this narrows the reading and cannot hide a file the branch actually changed.
147
- //
148
- // In a checkout sitting on the default branch the merge base *is* that branch's commit, so nothing
149
- // changes there. A repository with no common ancestor to find falls back to the previous reading
150
- // rather than failing the callers, all of which treat a throw as "nothing can be reused".
151
- async function change_base(): Promise<string> {
152
- const default_branch = await default_branch_reference()
153
-
154
- try {
155
- return await git_spawn.read([MERGE_BASE_COMMAND, default_branch, 'HEAD'])
156
- } catch {
157
- return default_branch
158
- }
159
- }
160
-
161
- async function diff_main(file_path: string): Promise<string> {
162
- return await git_spawn.read(['diff', await change_base(), '--', file_path])
163
- }
164
-
165
- // Names only, for callers that classify a change rather than read it — `josh review:level` decides
166
- // the review depth from the paths alone, and reading the whole diff to get them would be the
167
- // expensive half of the thing it exists to make cheaper (joshuafolkken/kit#966).
168
- //
169
- // `core.quotePath=false` is not cosmetic. With git's default, a path containing any non-ASCII byte
170
- // comes back C-quoted — `"prompts/\343\202\263.md"` — and a classifier testing `startsWith('prompts/')`
171
- // against a string that begins with a quote character answers no. `review:level` fails safe there
172
- // (non-inert wins), but a classifier that answered `skip` would drop a change it is meant to catch,
173
- // so the quoting is turned off at the source every reader shares (joshuafolkken/kit#907).
174
- const NO_PATH_QUOTING: ReadonlyArray<string> = ['-c', 'core.quotePath=false']
175
-
176
- // Repository-root-relative paths, whatever the checkout is configured to prefer. `diff.relative` is
177
- // a per-repository setting a person may have turned on for their own reading, and with it `git diff
178
- // --name-only` run from a subdirectory prints cwd-relative paths — which every caller of this
179
- // reading then joins onto the repository root (joshuafolkken/kit#1257). The flag pins the spelling
180
- // at the source rather than leaving each caller to discover the configuration.
181
- const NO_RELATIVE_PATHS = '--no-relative'
182
-
183
- // **Rename detection is turned off because this listing is a description of a tree, not a summary of
184
- // a change** (joshuafolkken/kit#1533). With git's default `diff.renames=true`, a rename prints only
185
- // its *destination*: `git diff --name-only <base>` across a commit that moved `scripts/init-logic.ts`
186
- // to `scripts/init/init-logic.ts` names the second path and never the first.
187
- //
188
- // That omission is what breaks `josh gate`'s reuse. The green record it compares is `base` plus a
189
- // digest per listed path, and the whole claim that the record still describes the tree rests on one
190
- // property: **every path that differs from `base` is in the list**, so `base` plus the digests
191
- // determines the tracked tree completely. A rename's source path differs from `base` — it is gone —
192
- // and it is not in the list. So a tree in which the file was renamed away and a tree in which it has
193
- // since come back produce the *same* record: the returned file matches `base`, so it enters no diff,
194
- // while the destination is an addition either way. The gate then reuses a green taken on the first
195
- // tree for the second, without running one check on it, and a duplicated module that lint, the type
196
- // check and the unit suite would all have failed on is committed on a "passed".
197
- //
198
- // Off, a rename is listed as its two halves — a delete and an add — and the delete is what makes the
199
- // two trees compare unequal. `review-tree.ts` already records a listed path the tree does not hold
200
- // as `ABSENT_DIGEST` rather than dropping it, which is exactly the entry this flag produces.
201
- //
202
- // **Every other reader moves in the safe direction.** `josh review:level` sees
203
- // one more path and can only widen; `josh lint:related` and `josh test:related` drop what the tree
204
- // no longer holds through `changed-file-scope.ts`, which they already had to do for a plain delete.
205
- const NO_RENAME_DETECTION = '--no-renames'
206
-
207
- // The commit `change_base` resolves to. Every "changed" reading below is a diff against it, so a set
208
- // of changed paths — or a map of their digests — means nothing without it: fetch an advanced default
209
- // branch and rebase onto it, and each digest can stay identical while the rest of the tree is
210
- // replaced by code no check has read (joshuafolkken/kit#1328). A rebase still invalidates a stamp
211
- // taken against it — the rebase moves `HEAD`, and with it the merge base — while another lane
212
- // merging into the shared default branch no longer does, because it moves neither
213
- // (joshuafolkken/kit#1527).
214
- async function change_base_commit(): Promise<string> {
215
- return await git_spawn.read(['rev-parse', await change_base()])
216
- }
217
-
218
- async function diff_main_names(): Promise<string> {
219
- return await git_spawn.read([
220
- ...NO_PATH_QUOTING,
221
- 'diff',
222
- NAME_ONLY_FLAG,
223
- NO_RELATIVE_PATHS,
224
- NO_RENAME_DETECTION,
225
- await change_base(),
226
- '--',
227
- ])
228
- }
229
-
230
- async function diff_cached_names(): Promise<string> {
231
- return await git_spawn.read([
232
- ...NO_PATH_QUOTING,
233
- 'diff',
234
- '--cached',
235
- NAME_ONLY_FLAG,
236
- NO_RELATIVE_PATHS,
237
- NO_RENAME_DETECTION,
238
- ])
239
- }
240
-
241
- // The branch diff as `added\tdeleted\tpath` rows — the same base and flags as `diff_main_names`, so
242
- // the size read against and the name read agree on what "changed" is. A binary file prints `-` for
243
- // both counts; the parser above `josh split:assess` reads it treats those as zero changed lines.
244
- async function diff_main_numstat(): Promise<string> {
245
- return await git_spawn.read([
246
- ...NO_PATH_QUOTING,
247
- 'diff',
248
- '--numstat',
249
- NO_RELATIVE_PATHS,
250
- NO_RENAME_DETECTION,
251
- await change_base(),
252
- '--',
253
- ])
254
- }
255
-
256
- // Files git is not tracking yet. `git diff` never lists them, so a classifier built on the diff
257
- // alone sees a change that adds a whole new module as an empty one — which is how a run adding new
258
- // code could have been handed a reduced review level (joshuafolkken/kit#966).
259
- //
260
- // **`--full-name` and the `:/` pathspec are what make this reading agree with the diff beside it**
261
- // (joshuafolkken/kit#1257). `git diff --name-only` prints repository-root-relative paths for the
262
- // whole tree wherever it is run; `git ls-files --others` prints *cwd*-relative paths and lists only
263
- // what is below cwd. Read from a subdirectory, the two halves of `changed_paths` therefore came
264
- // back in two different coordinate systems, and every caller that resolves a path against the
265
- // repository root — `review-tree`, `josh test:related` — turned a new file into one that does not
266
- // exist, or, where the same tail exists elsewhere in the tree, into a different file entirely. The
267
- // pathspec restores the whole tree; the flag restores the root-relative spelling.
268
- const WHOLE_TREE_PATHSPEC = ':/'
269
-
270
- async function untracked_names(): Promise<string> {
271
- return await git_spawn.read([
272
- ...NO_PATH_QUOTING,
273
- 'ls-files',
274
- '--others',
275
- '--exclude-standard',
276
- '--full-name',
277
- WHOLE_TREE_PATHSPEC,
278
- ])
279
- }
280
-
281
78
  // One branch from `origin`, fetched by name. `gh pr checkout` did this itself after resolving the
282
79
  // head branch through GraphQL, which a cloud session is answered 403 for (joshuafolkken/kit#1022);
283
80
  // the resolution moved to REST and the fetch is spelled out here instead of being re-wrapped.
@@ -380,22 +177,33 @@ function is_upstream_not_set_error(error: unknown): boolean {
380
177
  return cause !== undefined && is_exit_code_128(cause)
381
178
  }
382
179
 
180
+ const NO_VERIFY_FLAG = '--no-verify'
181
+
383
182
  // Both pushes go through `git_push_transport` rather than `git_spawn.with_output`, which is
384
183
  // what gives them a timeout and an SSH keepalive the local git commands beside them do not need
385
184
  // (joshuafolkken/kit#1251). The thrown error keeps the same `cause.exit_code` shape, so the 128
386
185
  // fallback below reads it exactly as it did.
387
- async function push_with_upstream(branch_name: string): Promise<void> {
388
- await git_push_transport.push(['--set-upstream', 'origin', branch_name])
186
+ async function push_with_upstream(
187
+ branch_name: string,
188
+ verify_flags: ReadonlyArray<string>,
189
+ ): Promise<void> {
190
+ await git_push_transport.push([...verify_flags, '--set-upstream', 'origin', branch_name])
389
191
  }
390
192
 
193
+ // **The push budget bounds the transfer alone** (joshuafolkken/kit#3300). The pre-push hook runs once,
194
+ // unbounded, before either push and outside the `try`; both transfers then skip it with `--no-verify`,
195
+ // so neither the timeout retry nor the `--set-upstream` fallback repeats it, and a failed hook is not
196
+ // mistaken for a missing upstream. A git too old to run the hook ahead leaves it to the push, as before.
391
197
  async function push(): Promise<void> {
198
+ const verify_flags = (await git_pre_push_hook.run()) ? [NO_VERIFY_FLAG] : []
199
+
392
200
  try {
393
- await git_push_transport.push([])
201
+ await git_push_transport.push([...verify_flags])
394
202
  } catch (error) {
395
203
  if (is_upstream_not_set_error(error)) {
396
204
  const current_branch = await branch()
397
205
 
398
- await push_with_upstream(current_branch)
206
+ await push_with_upstream(current_branch, verify_flags)
399
207
 
400
208
  return
401
209
  }
@@ -566,15 +374,9 @@ const git_command = {
566
374
  head_commit,
567
375
  git_directories,
568
376
  diff_cached,
569
- diff_cached_names,
570
- diff_main,
571
- change_base,
572
- change_base_commit,
573
- diff_main_names,
574
- diff_main_numstat,
575
- untracked_names,
576
- get_default_branch,
577
- default_branch_reference,
377
+ // The default branch, the change base and every reading against it are `git-diff-reads.ts`'s
378
+ // (joshuafolkken/kit#3264), re-exported here so no call site, mock or spy moved with them.
379
+ ...git_diff_reads,
578
380
  fetch_branch,
579
381
  merge_fast_forward,
580
382
  fast_forward_local,
@@ -0,0 +1,225 @@
1
+ import { git_spawn } from './git-spawn'
2
+
3
+ // The default branch, the base a change is measured against, and every "changed" reading taken
4
+ // against that base — split out of `git-command.ts` when it reached its line limit
5
+ // (joshuafolkken/kit#3264). `git_command` re-exports each one under the name it always had, so the
6
+ // move changed no call site and no suite that mocks or spies on `git_command`.
7
+
8
+ const REFS_REMOTES_ORIGIN_PREFIX = 'refs/remotes/origin/'
9
+ const NAME_ONLY_FLAG = '--name-only'
10
+ const DEFAULT_BRANCH_FALLBACK = 'main'
11
+
12
+ async function get_default_branch(): Promise<string> {
13
+ try {
14
+ const output = await git_spawn.read(['symbolic-ref', 'refs/remotes/origin/HEAD'])
15
+ const trimmed = output.trim()
16
+
17
+ if (trimmed.startsWith(REFS_REMOTES_ORIGIN_PREFIX)) {
18
+ return trimmed.slice(REFS_REMOTES_ORIGIN_PREFIX.length)
19
+ }
20
+ } catch {
21
+ // fall through to default
22
+ }
23
+
24
+ return DEFAULT_BRANCH_FALLBACK
25
+ }
26
+
27
+ const MERGE_BASE_COMMAND = 'merge-base'
28
+
29
+ // **The ref the default branch is actually at**, which is not the same thing as its name
30
+ // (joshuafolkken/kit#1535). `get_default_branch` returns the bare name, and git resolves that to the
31
+ // *local* `refs/heads/<default>` — a ref nothing in this workflow advances, because merges happen on
32
+ // GitHub and the default branch is checked out in no work tree. This is the single source both the
33
+ // lane's start point and `change_base` below resolve through, so a lane cut from
34
+ // `refs/remotes/origin/<default>` is measured against that same commit. Measured without it: a lane
35
+ // cut from a remote-tracking ref three commits ahead reported those three commits' files as its own.
36
+ //
37
+ // It falls back to the bare name where there is no remote-tracking ref — a fresh `git init`, a clone
38
+ // with no `origin` — which is the reading every caller had before. The one case it reads differently
39
+ // is a local default branch *ahead* of the remote, which this workflow does not produce and
40
+ // `prevent-main-commit.ts` exists to stop.
41
+ async function reference_exists(reference: string): Promise<boolean> {
42
+ try {
43
+ await git_spawn.read(['rev-parse', '--verify', '--quiet', reference])
44
+
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ async function default_branch_reference(): Promise<string> {
52
+ const default_branch = await get_default_branch()
53
+ const remote_reference = `${REFS_REMOTES_ORIGIN_PREFIX}${default_branch}`
54
+
55
+ return (await reference_exists(remote_reference)) ? remote_reference : default_branch
56
+ }
57
+
58
+ // The commit this branch was cut from — and the base every "changed" reading below measures
59
+ // against, in place of the default branch itself.
60
+ //
61
+ // **A two-dot `git diff <default-branch>` compares a moving *ref* to the working tree, and in a
62
+ // linked work tree that ref is shared with every other lane** (joshuafolkken/kit#1527). So when one
63
+ // lane merges, an unmerged lane's diff picks that lane's files up in reverse: files this branch
64
+ // never touched arrive in `josh review:brief`'s target list, in the digest map `josh gate` stamps,
65
+ // and in `josh review:round2`'s fix delta.
66
+ //
67
+ // **The merge base is the fixed point that removes it**, because it is a commit rather than a ref:
68
+ // an advance of the default branch that this branch is an ancestor of does not move it. It is also
69
+ // the right answer at both points of a run, and a lane passes through both — with the work still
70
+ // uncommitted `HEAD` is the cut commit and the merge base is that same commit, so the diff is the
71
+ // uncommitted work; once the branch commits, `HEAD` moves ahead while the merge base stays put, so
72
+ // the diff is the branch's whole change plus anything still uncommitted. `<default-branch>...HEAD`
73
+ // answers neither, because a three-dot diff ends at `HEAD` and a lane's work is uncommitted for
74
+ // most of its run.
75
+ //
76
+ // **It never reports less than the old reading did.** The new set is the branch's own change; the
77
+ // old one was that same set plus the inverse of whatever the default branch had advanced by — so
78
+ // this narrows the reading and cannot hide a file the branch actually changed.
79
+ //
80
+ // In a checkout sitting on the default branch the merge base *is* that branch's commit, so nothing
81
+ // changes there. A repository with no common ancestor to find falls back to the previous reading
82
+ // rather than failing the callers, all of which treat a throw as "nothing can be reused".
83
+ async function change_base(): Promise<string> {
84
+ const default_branch = await default_branch_reference()
85
+
86
+ try {
87
+ return await git_spawn.read([MERGE_BASE_COMMAND, default_branch, 'HEAD'])
88
+ } catch {
89
+ return default_branch
90
+ }
91
+ }
92
+
93
+ async function diff_main(file_path: string): Promise<string> {
94
+ return await git_spawn.read(['diff', await change_base(), '--', file_path])
95
+ }
96
+
97
+ // Names only, for callers that classify a change rather than read it — `josh review:level` decides
98
+ // the review depth from the paths alone, and reading the whole diff to get them would be the
99
+ // expensive half of the thing it exists to make cheaper (joshuafolkken/kit#966).
100
+ //
101
+ // `core.quotePath=false` is not cosmetic. With git's default, a path containing any non-ASCII byte
102
+ // comes back C-quoted — `"prompts/\343\202\263.md"` — and a classifier testing `startsWith('prompts/')`
103
+ // against a string that begins with a quote character answers no. `review:level` fails safe there
104
+ // (non-inert wins), but a classifier that answered `skip` would drop a change it is meant to catch,
105
+ // so the quoting is turned off at the source every reader shares (joshuafolkken/kit#907).
106
+ const NO_PATH_QUOTING: ReadonlyArray<string> = ['-c', 'core.quotePath=false']
107
+
108
+ // Repository-root-relative paths, whatever the checkout is configured to prefer. `diff.relative` is
109
+ // a per-repository setting a person may have turned on for their own reading, and with it `git diff
110
+ // --name-only` run from a subdirectory prints cwd-relative paths — which every caller of this
111
+ // reading then joins onto the repository root (joshuafolkken/kit#1257). The flag pins the spelling
112
+ // at the source rather than leaving each caller to discover the configuration.
113
+ const NO_RELATIVE_PATHS = '--no-relative'
114
+
115
+ // **Rename detection is turned off because this listing is a description of a tree, not a summary of
116
+ // a change** (joshuafolkken/kit#1533). With git's default `diff.renames=true`, a rename prints only
117
+ // its *destination*: `git diff --name-only <base>` across a commit that moved `scripts/init-logic.ts`
118
+ // to `scripts/init/init-logic.ts` names the second path and never the first.
119
+ //
120
+ // That omission is what breaks `josh gate`'s reuse. The green record it compares is `base` plus a
121
+ // digest per listed path, and the whole claim that the record still describes the tree rests on one
122
+ // property: **every path that differs from `base` is in the list**, so `base` plus the digests
123
+ // determines the tracked tree completely. A rename's source path differs from `base` — it is gone —
124
+ // and it is not in the list. So a tree in which the file was renamed away and a tree in which it has
125
+ // since come back produce the *same* record: the returned file matches `base`, so it enters no diff,
126
+ // while the destination is an addition either way. The gate then reuses a green taken on the first
127
+ // tree for the second, without running one check on it, and a duplicated module that lint, the type
128
+ // check and the unit suite would all have failed on is committed on a "passed".
129
+ //
130
+ // Off, a rename is listed as its two halves — a delete and an add — and the delete is what makes the
131
+ // two trees compare unequal. `review-tree.ts` already records a listed path the tree does not hold
132
+ // as `ABSENT_DIGEST` rather than dropping it, which is exactly the entry this flag produces.
133
+ //
134
+ // **Every other reader moves in the safe direction.** `josh review:level` sees
135
+ // one more path and can only widen; `josh lint:related` and `josh test:related` drop what the tree
136
+ // no longer holds through `changed-file-scope.ts`, which they already had to do for a plain delete.
137
+ const NO_RENAME_DETECTION = '--no-renames'
138
+
139
+ // The commit `change_base` resolves to. Every "changed" reading below is a diff against it, so a set
140
+ // of changed paths — or a map of their digests — means nothing without it: fetch an advanced default
141
+ // branch and rebase onto it, and each digest can stay identical while the rest of the tree is
142
+ // replaced by code no check has read (joshuafolkken/kit#1328). A rebase still invalidates a stamp
143
+ // taken against it — the rebase moves `HEAD`, and with it the merge base — while another lane
144
+ // merging into the shared default branch no longer does, because it moves neither
145
+ // (joshuafolkken/kit#1527).
146
+ async function change_base_commit(): Promise<string> {
147
+ return await git_spawn.read(['rev-parse', await change_base()])
148
+ }
149
+
150
+ async function diff_main_names(): Promise<string> {
151
+ return await git_spawn.read([
152
+ ...NO_PATH_QUOTING,
153
+ 'diff',
154
+ NAME_ONLY_FLAG,
155
+ NO_RELATIVE_PATHS,
156
+ NO_RENAME_DETECTION,
157
+ await change_base(),
158
+ '--',
159
+ ])
160
+ }
161
+
162
+ async function diff_cached_names(): Promise<string> {
163
+ return await git_spawn.read([
164
+ ...NO_PATH_QUOTING,
165
+ 'diff',
166
+ '--cached',
167
+ NAME_ONLY_FLAG,
168
+ NO_RELATIVE_PATHS,
169
+ NO_RENAME_DETECTION,
170
+ ])
171
+ }
172
+
173
+ // The branch diff as `added\tdeleted\tpath` rows — the same base and flags as `diff_main_names`, so
174
+ // the size read against and the name read agree on what "changed" is. A binary file prints `-` for
175
+ // both counts; the parser above `josh split:assess` reads it treats those as zero changed lines.
176
+ async function diff_main_numstat(): Promise<string> {
177
+ return await git_spawn.read([
178
+ ...NO_PATH_QUOTING,
179
+ 'diff',
180
+ '--numstat',
181
+ NO_RELATIVE_PATHS,
182
+ NO_RENAME_DETECTION,
183
+ await change_base(),
184
+ '--',
185
+ ])
186
+ }
187
+
188
+ // Files git is not tracking yet. `git diff` never lists them, so a classifier built on the diff
189
+ // alone sees a change that adds a whole new module as an empty one — which is how a run adding new
190
+ // code could have been handed a reduced review level (joshuafolkken/kit#966).
191
+ //
192
+ // **`--full-name` and the `:/` pathspec are what make this reading agree with the diff beside it**
193
+ // (joshuafolkken/kit#1257). `git diff --name-only` prints repository-root-relative paths for the
194
+ // whole tree wherever it is run; `git ls-files --others` prints *cwd*-relative paths and lists only
195
+ // what is below cwd. Read from a subdirectory, the two halves of `changed_paths` therefore came
196
+ // back in two different coordinate systems, and every caller that resolves a path against the
197
+ // repository root — `review-tree`, `josh test:related` — turned a new file into one that does not
198
+ // exist, or, where the same tail exists elsewhere in the tree, into a different file entirely. The
199
+ // pathspec restores the whole tree; the flag restores the root-relative spelling.
200
+ const WHOLE_TREE_PATHSPEC = ':/'
201
+
202
+ async function untracked_names(): Promise<string> {
203
+ return await git_spawn.read([
204
+ ...NO_PATH_QUOTING,
205
+ 'ls-files',
206
+ '--others',
207
+ '--exclude-standard',
208
+ '--full-name',
209
+ WHOLE_TREE_PATHSPEC,
210
+ ])
211
+ }
212
+
213
+ const git_diff_reads = {
214
+ change_base,
215
+ change_base_commit,
216
+ default_branch_reference,
217
+ diff_cached_names,
218
+ diff_main,
219
+ diff_main_names,
220
+ diff_main_numstat,
221
+ get_default_branch,
222
+ untracked_names,
223
+ }
224
+
225
+ export { git_diff_reads }
@@ -12,21 +12,15 @@ function get_stderr_from_error(cause: Error): string | undefined {
12
12
  function get_error_message_from_cause(cause: Error): string | undefined {
13
13
  const message = cause.message.trim()
14
14
 
15
- if (message.length > 0) {
16
- return message
17
- }
15
+ if (message.length > 0) return message
18
16
 
19
17
  return get_stderr_from_error(cause)
20
18
  }
21
19
 
22
20
  function get_cause_message(cause: unknown): string | undefined {
23
- if (cause instanceof Error) {
24
- return get_error_message_from_cause(cause)
25
- }
21
+ if (cause instanceof Error) return get_error_message_from_cause(cause)
26
22
 
27
- if (typeof cause === 'string') {
28
- return cause.trim()
29
- }
23
+ if (typeof cause === 'string') return cause.trim()
30
24
 
31
25
  return undefined
32
26
  }
@@ -34,9 +28,7 @@ function get_cause_message(cause: unknown): string | undefined {
34
28
  function display_error_details(cause: unknown): void {
35
29
  const cause_message = get_cause_message(cause)
36
30
 
37
- if (cause_message === undefined || cause_message.length === 0) {
38
- return
39
- }
31
+ if (cause_message === undefined || cause_message.length === 0) return
40
32
 
41
33
  console.error('')
42
34
  console.error('💡 Details:', cause_message)
@@ -0,0 +1,139 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { git_spawn } from './git-spawn'
5
+
6
+ // The pre-push hook, run ahead of the push instead of inside it (joshuafolkken/kit#3300).
7
+ //
8
+ // **The push budget is a transfer budget, and a hook is not a transfer.** `PUSH_TIMEOUT_MS` was read
9
+ // off what moving objects costs here, but `git push` runs the pre-push hook first and the budget
10
+ // counted it: a hook that ran the whole unit suite — any push the gate's record does not cover, such
11
+ // as the observation ledger commit or a tree `ship` has just synced with main — took 130 to 196
12
+ // seconds and was killed at 120 on a push that was healthy. Killing `git` did not kill the hook
13
+ // either: lefthook and vitest outlived it, and the retry started a second suite in the same work tree,
14
+ // where each `test-state-guard` read the other's temporary files as a mutation and both failed.
15
+ //
16
+ // So the hook runs here, once, with no budget, and `git_command.push` spawns the bounded transfer that
17
+ // follows with `--no-verify`. The retry then repeats the transfer alone, and a hook that is still
18
+ // running cannot exist for it to overlap.
19
+ //
20
+ // **What git hands the hook is reproduced rather than dropped.** `git hook run` executes whatever
21
+ // hook is configured — lefthook's or anyone's — and the arguments and stdin are git's pre-push
22
+ // contract: the remote's name and URL, and one `<local ref> <local oid> <remote ref> <remote oid>`
23
+ // line per ref. A hook that reads them (`git lfs pre-push` uploads exactly the objects those lines
24
+ // name) would otherwise act on nothing. Both pushes `git_command.push` makes send the current branch to
25
+ // the same name on `origin`, so that is the one line written. Like git, an up-to-date branch runs no
26
+ // hook: the remote-tracking ref already naming `HEAD` is git's "Everything up-to-date".
27
+
28
+ const REMOTE = 'origin'
29
+ const HOOK_NAME = 'pre-push'
30
+ const PROBE_HOOK_NAME = 'kit-hook-run-probe'
31
+ const ALLOW_UNKNOWN_HOOK_NAME_FLAG = '--allow-unknown-hook-name'
32
+ const HOOK_RUN = 'run'
33
+ const STDIN_FILE = 'refs'
34
+ const TEMPORARY_PREFIX = 'kit-pre-push-'
35
+ const ZERO_DIGIT = '0'
36
+
37
+ interface PushReferences {
38
+ branch: string
39
+ local_oid: string
40
+ remote_oid: string | undefined
41
+ }
42
+
43
+ // `undefined` is a branch the remote has never seen, which git reports with an all-zero object id.
44
+ async function read_remote_oid(branch: string): Promise<string | undefined> {
45
+ try {
46
+ return await git_spawn.read([
47
+ 'rev-parse',
48
+ '--verify',
49
+ '--quiet',
50
+ `refs/remotes/${REMOTE}/${branch}`,
51
+ ])
52
+ } catch {
53
+ return undefined
54
+ }
55
+ }
56
+
57
+ async function read_push_references(): Promise<PushReferences> {
58
+ const [branch, local_oid] = await Promise.all([
59
+ git_spawn.read(['rev-parse', '--abbrev-ref', 'HEAD']),
60
+ git_spawn.read(['rev-parse', 'HEAD']),
61
+ ])
62
+
63
+ return { branch, local_oid, remote_oid: await read_remote_oid(branch) }
64
+ }
65
+
66
+ // The zero id takes the local id's length, so a SHA-256 repository gets 64 digits rather than 40.
67
+ function to_stdin_line(references: PushReferences): string {
68
+ const reference = `refs/heads/${references.branch}`
69
+ const remote_oid = references.remote_oid ?? ZERO_DIGIT.repeat(references.local_oid.length)
70
+
71
+ return `${reference} ${references.local_oid} ${reference} ${remote_oid}\n`
72
+ }
73
+
74
+ function to_hook_run_arguments(stdin_path: string, hook_name: string): Array<string> {
75
+ return [HOOK_RUN, '--ignore-missing', `--to-stdin=${stdin_path}`, hook_name]
76
+ }
77
+
78
+ // **A git that cannot run the hook this way leaves it to the push** — `git hook run` is Git 2.36, and
79
+ // `--to-stdin` later still; an older git exits 129 on either. It is asked with a hook name nothing
80
+ // configures, which `--ignore-missing` turns into a clean exit on any git that understands the call,
81
+ // so the answer never depends on what the real hook does. Git 2.54 refuses a name that is not a
82
+ // native hook unless `--allow-unknown-hook-name` says otherwise, and a git before it refuses that flag,
83
+ // so the probe is asked both ways.
84
+ async function can_probe(probe_arguments: ReadonlyArray<string>): Promise<boolean> {
85
+ try {
86
+ await git_spawn.read(['hook', ...probe_arguments])
87
+
88
+ return true
89
+ } catch {
90
+ return false
91
+ }
92
+ }
93
+
94
+ async function can_run_hooks(stdin_path: string): Promise<boolean> {
95
+ const [, ...options] = to_hook_run_arguments(stdin_path, PROBE_HOOK_NAME)
96
+
97
+ if (await can_probe([HOOK_RUN, ALLOW_UNKNOWN_HOOK_NAME_FLAG, ...options])) return true
98
+
99
+ return await can_probe([HOOK_RUN, ...options])
100
+ }
101
+
102
+ async function run_with_stdin(stdin_line: string): Promise<boolean> {
103
+ const directory = await mkdtemp(path.join(tmpdir(), TEMPORARY_PREFIX))
104
+ const stdin_path = path.join(directory, STDIN_FILE)
105
+
106
+ try {
107
+ await writeFile(stdin_path, stdin_line)
108
+ if (!(await can_run_hooks(stdin_path))) return false
109
+ const url = await git_spawn.read(['remote', 'get-url', REMOTE])
110
+
111
+ await git_spawn.with_output('hook', [
112
+ ...to_hook_run_arguments(stdin_path, HOOK_NAME),
113
+ '--',
114
+ REMOTE,
115
+ url,
116
+ ])
117
+
118
+ return true
119
+ } finally {
120
+ await rm(directory, { force: true, recursive: true })
121
+ }
122
+ }
123
+
124
+ // `true` when the hook is settled here — run, or nothing to push — so the push skips it; `false` when
125
+ // this git cannot run it, so the push runs it as it always did. A hook that fails throws
126
+ // `git hook exited with code <n>`, and the push is never attempted.
127
+ async function run(): Promise<boolean> {
128
+ const references = await read_push_references()
129
+
130
+ if (references.local_oid === references.remote_oid) return true
131
+
132
+ return await run_with_stdin(to_stdin_line(references))
133
+ }
134
+
135
+ const git_pre_push_hook = {
136
+ run,
137
+ }
138
+
139
+ export { git_pre_push_hook }