@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
@@ -9,26 +9,29 @@ import { git_gh_issue_list, MAX_SCANNED } from '#scripts/gh/git-gh-issue-list'
9
9
  import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
10
10
  import { github_issue_url } from '#scripts/gh/github-issue-url'
11
11
  import { error_text } from '#scripts/lib/error-message'
12
+ import type { PollOptions } from '#scripts/lib/poll'
12
13
  import { repository_labels } from '#scripts/repo/repository-labels'
13
14
  import { issue_auto_ok } from './issue-auto-ok'
14
15
  import { issue_file, type FileArguments } from './issue-file'
15
16
  import { issue_lint_cli } from './issue-lint-cli'
17
+ import { issue_release_cli } from './issue-release-cli'
16
18
  import { issue_scout_cli } from './issue-scout-cli'
17
19
  import { issue_wip } from './issue-wip'
18
20
 
19
21
  // `josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <route>] [--label <name>]…
20
- // [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]` — file an Issue with every
21
- // filing step run in order (joshuafolkken/kit#2808): the third-party refusal, the body lint, the
22
- // `## Origin` check for another repository, the WIP cap count (joshuafolkken/kit#3181), the duplicate
23
- // scout, the missing workflow labels created (joshuafolkken/kit#3176), the `auto-ok` decision
24
- // (joshuafolkken/kit#3213), the create call carrying every label, and `epic:bundle` after it.
22
+ // [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]` — file an Issue
23
+ // with every filing step run in order (joshuafolkken/kit#2808): the third-party refusal, the body
24
+ // lint, the `## Origin` check for another repository, the `auto-ok` decision (joshuafolkken/kit#3213)
25
+ // with the run label it owes (joshuafolkken/kit#3313), the WIP cap count (joshuafolkken/kit#3181), the
26
+ // duplicate scout, the missing workflow labels created (joshuafolkken/kit#3176), the create call
27
+ // carrying every label, the release link on `--release` (joshuafolkken/kit#3360), and `epic:bundle`.
25
28
  // A direct `gh api …/issues` filing is refused by the `direct-filing` delivered rule and pointed here.
26
29
 
27
30
  const SUCCESS_EXIT_CODE = 0
28
31
  const FAILURE_EXIT_CODE = 1
29
32
  const ARGV_OFFSET = 2
30
33
  const USAGE =
31
- 'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok]'
34
+ 'Usage: josh issue:file "<title>" --body-file <path> --depth <0|1|2> [--route <tier-a|split|interrupt|review-cap>] [--label <name>]… [--repo <owner/repo>] [--distinct <N,…>] [--over-cap] [--no-auto-ok] [--release]'
32
35
  const UNKNOWN_REPO_MESSAGE =
33
36
  'Could not read this repository from `git remote`, so the filing has no repository to compare against — check `gh auth status`.'
34
37
  const THIRD_PARTY_MESSAGE =
@@ -36,6 +39,9 @@ const THIRD_PARTY_MESSAGE =
36
39
  // `GH_REPO` is the variable `gh` reads `{owner}/{repo}` from, so setting it points every listing the
37
40
  // scout and `epic:bundle` make at the target repository rather than at this checkout.
38
41
  const GH_REPO_VARIABLE = 'GH_REPO'
42
+ // The open listing `epic:bundle` reads trails the create call, so the issue just filed is looked for
43
+ // again for up to ten seconds before the placement is given up (joshuafolkken/kit#3332).
44
+ const FRESH_ISSUE_POLL: PollOptions = { attempts: 6, interval_ms: 2000 }
39
45
 
40
46
  interface Filing {
41
47
  args: FileArguments
@@ -130,22 +136,35 @@ async function is_scout_clear(filing: Filing): Promise<boolean> {
130
136
  return open.length === 0
131
137
  }
132
138
 
133
- // `undefined` when the create call failed. `gh`'s standard error is captured into the thrown error
134
- // rather than printed, so it is printed here — otherwise the filing fails with no reason given. A label
135
- // the create applies is provisioned first, so it never arrives with a generated color.
136
- async function create(filing: Filing): Promise<string | undefined> {
137
- repository_labels.ensure_labels(filing.target)
139
+ // Every label the create call carries, or `undefined` when an `auto-ok` filing names neither run label
140
+ // (joshuafolkken/kit#3313). Decided before the WIP count and the scout, so a refusal costs no listing.
141
+ async function labels_of(filing: Filing): Promise<ReadonlyArray<string> | undefined> {
138
142
  const auto_ok = await issue_auto_ok.resolve(
139
143
  filing.args.is_auto_ok_opted_out,
140
144
  filing.target,
141
145
  filing.current,
146
+ filing.args.labels,
142
147
  )
143
148
 
144
149
  console.info(issue_auto_ok.line_of(auto_ok))
150
+ const labels = issue_file.labels_of(filing.args, filing.body, auto_ok.is_applied)
151
+ const problem = issue_file.triage_problem(labels)
152
+
153
+ if (problem === undefined) return labels
154
+ console.error(`✖ ${problem}`)
155
+
156
+ return undefined
157
+ }
158
+
159
+ // `undefined` when the create call failed. `gh`'s standard error is captured into the thrown error
160
+ // rather than printed, so it is printed here — otherwise the filing fails with no reason given. A label
161
+ // the create applies is provisioned first, so it never arrives with a generated color.
162
+ async function create(filing: Filing, labels: ReadonlyArray<string>): Promise<string | undefined> {
163
+ repository_labels.ensure_labels(filing.target)
145
164
  const request = git_gh_issue_write.issue_create_request({
146
165
  title: filing.args.title,
147
166
  body: filing.body,
148
- labels: issue_file.labels_of(filing.args, filing.body, auto_ok.is_applied),
167
+ labels,
149
168
  repo: filing.target,
150
169
  })
151
170
 
@@ -160,12 +179,32 @@ async function create(filing: Filing): Promise<string | undefined> {
160
179
  }
161
180
  }
162
181
 
182
+ function issue_number_of(url: string): number {
183
+ return Number(github_issue_url.parse(url)?.issue_number)
184
+ }
185
+
186
+ // `--release`: the filed Issue blocks the target's release Issue (joshuafolkken/kit#3360). Reported,
187
+ // never failed, for the reason `place` gives. The re-run hint names the target through `GH_REPO`:
188
+ // `issue:release` links in the repository `gh` resolves, which is not the target on a `--repo` filing.
189
+ async function link_release(url: string, filing: Filing): Promise<void> {
190
+ if (!filing.args.is_release) return
191
+ const issue_number = issue_number_of(url)
192
+ const is_linked =
193
+ Number.isSafeInteger(issue_number) &&
194
+ (await issue_release_cli.link(issue_number, filing.target))
195
+
196
+ if (is_linked) return
197
+ console.error(
198
+ `⚠ the release link did not complete for ${url} — run \`${GH_REPO_VARIABLE}=${filing.target} pnpm josh issue:release <N>\`.`,
199
+ )
200
+ }
201
+
163
202
  // The Issue exists once `create` returns, so a failed placement is reported rather than failed: a
164
203
  // non-zero exit would read as "not filed" and invite the second filing the scout exists to stop.
165
204
  async function place(url: string, target: string): Promise<void> {
166
- const issue_number = Number(github_issue_url.parse(url)?.issue_number)
205
+ const issue_number = issue_number_of(url)
167
206
  const exit_code = Number.isSafeInteger(issue_number)
168
- ? await epic_bundle_cli.report_for(issue_number, target)
207
+ ? await epic_bundle_cli.report_for(issue_number, target, FRESH_ISSUE_POLL)
169
208
  : FAILURE_EXIT_CODE
170
209
 
171
210
  if (exit_code !== SUCCESS_EXIT_CODE) {
@@ -173,25 +212,36 @@ async function place(url: string, target: string): Promise<void> {
173
212
  }
174
213
  }
175
214
 
176
- async function file(filing: Filing): Promise<number> {
215
+ function is_admitted(filing: Filing): boolean {
177
216
  const refusals = refusals_of(filing)
178
217
 
179
- if (refusals.length > 0) {
180
- console.error(refusals.join('\n'))
218
+ if (refusals.length === 0) return true
219
+ console.error(refusals.join('\n'))
181
220
 
182
- return FAILURE_EXIT_CODE
183
- }
221
+ return false
222
+ }
184
223
 
185
- if (!(await is_wip_clear(filing)) || !(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
186
- const url = await create(filing)
224
+ async function send(filing: Filing, labels: ReadonlyArray<string>): Promise<number> {
225
+ const url = await create(filing, labels)
187
226
 
188
227
  if (url === undefined) return FAILURE_EXIT_CODE
189
228
  console.info(url)
229
+ await link_release(url, filing)
190
230
  await place(url, filing.target)
191
231
 
192
232
  return SUCCESS_EXIT_CODE
193
233
  }
194
234
 
235
+ async function file(filing: Filing): Promise<number> {
236
+ if (!is_admitted(filing)) return FAILURE_EXIT_CODE
237
+ const labels = await labels_of(filing)
238
+
239
+ if (labels === undefined) return FAILURE_EXIT_CODE
240
+ if (!(await is_wip_clear(filing)) || !(await is_scout_clear(filing))) return FAILURE_EXIT_CODE
241
+
242
+ return await send(filing, labels)
243
+ }
244
+
195
245
  async function filing_of(args: FileArguments): Promise<Filing | string> {
196
246
  const body = await read_body(args.body_file)
197
247
 
@@ -228,7 +278,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
228
278
  process.exitCode = await run(argv)
229
279
  }
230
280
 
231
- const issue_file_cli = { THIRD_PARTY_MESSAGE, run }
281
+ const issue_file_cli = { FRESH_ISSUE_POLL, THIRD_PARTY_MESSAGE, run }
232
282
 
233
283
  if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
234
284
 
@@ -1,5 +1,6 @@
1
1
  import type { parseArgs } from 'node:util'
2
2
  import { epic_issue } from '#scripts/epic/epic-issue'
3
+ import { epic_triage } from '#scripts/epic/epic-triage'
3
4
  import { github_issue_url } from '#scripts/gh/github-issue-url'
4
5
  import { cli_flags } from '#scripts/lib/cli-flags'
5
6
  import { issue_backlinks } from './issue-backlinks'
@@ -9,6 +10,8 @@ import {
9
10
  DEPTH_LABEL_ORDER,
10
11
  FILING_ROUTE_LABELS,
11
12
  has_label_name,
13
+ RUN_LANE_LABEL,
14
+ RUN_SOLO_LABEL,
12
15
  } from './issue-labels'
13
16
  import { markdown_section } from './markdown-section'
14
17
 
@@ -40,6 +43,9 @@ interface FileArguments {
40
43
  is_over_cap: boolean
41
44
  // `--no-auto-ok`: the Issue needs a person's judgement, so `auto-ok` is not applied (`issue-auto-ok.ts`).
42
45
  is_auto_ok_opted_out: boolean
46
+ // `--release`: the change reaches a consumer only once published, so the Issue is linked to the
47
+ // repository's release Issue as a blocker (`issue-release-cli.ts`).
48
+ is_release: boolean
43
49
  }
44
50
 
45
51
  const OPTIONS = {
@@ -51,6 +57,7 @@ const OPTIONS = {
51
57
  distinct: { type: 'string', multiple: true },
52
58
  'over-cap': { type: 'boolean' },
53
59
  'no-auto-ok': { type: 'boolean' },
60
+ release: { type: 'boolean' },
54
61
  } as const
55
62
 
56
63
  // The label a `--depth` / `--route` value names, or `undefined` when it names none — read against the
@@ -120,6 +127,7 @@ function arguments_of(values: ParsedValues, title: string | undefined): FileArgu
120
127
  repo: values.repo,
121
128
  is_over_cap: values['over-cap'] === true,
122
129
  is_auto_ok_opted_out: values['no-auto-ok'] === true,
130
+ is_release: values.release === true,
123
131
  }
124
132
  }
125
133
 
@@ -152,6 +160,19 @@ function labels_of(args: FileArguments, body: string, is_auto_ok = false): Reado
152
160
  return [...labels, AUTO_OK_LABEL]
153
161
  }
154
162
 
163
+ // Why an `auto-ok` filing with neither `run:lane` nor `run:solo` is refused, or `undefined` when it is
164
+ // not (joshuafolkken/kit#3313). The offer paths withhold every candidate while one opted-in Issue is
165
+ // untriaged, so a run's own filing without the judgement stopped every lane. No default is applied:
166
+ // `run:solo` is the answer that keeps a verification defect from running beside others, and only a
167
+ // reading of the Issue gives it (`backlogrun-lanes.md`).
168
+ function triage_problem(labels: ReadonlyArray<string>): string | undefined {
169
+ const is_owed = has_label_name(labels, AUTO_OK_LABEL) && !epic_triage.has_triage_label(labels)
170
+
171
+ if (!is_owed) return undefined
172
+
173
+ return `an \`${AUTO_OK_LABEL}\` filing needs \`--label ${RUN_LANE_LABEL}\` or \`--label ${RUN_SOLO_LABEL}\` — \`${RUN_SOLO_LABEL}\` only for a defect in kit's own verification that makes unrelated PRs answer wrongly on main now (\`backlogrun-lanes.md\`), else \`${RUN_LANE_LABEL}\``
174
+ }
175
+
155
176
  function is_same_repository(target: string, current: string): boolean {
156
177
  return target.toLowerCase() === current.toLowerCase()
157
178
  }
@@ -202,7 +223,14 @@ function unacknowledged(
202
223
  return candidates.filter((candidate) => !distinct.includes(candidate))
203
224
  }
204
225
 
205
- const issue_file = { parse, labels_of, origin_problem, unacknowledged, is_same_repository }
226
+ const issue_file = {
227
+ parse,
228
+ labels_of,
229
+ origin_problem,
230
+ triage_problem,
231
+ unacknowledged,
232
+ is_same_repository,
233
+ }
206
234
 
207
235
  export type { FileArguments }
208
236
  export { issue_file }
@@ -65,6 +65,12 @@ const NEEDS_HUMAN_REVIEW_LABEL = 'needs-human-review'
65
65
  // takes it off, because taking it off asserts the work is *not* done, which is the same claim in
66
66
  // reverse and is no more a run's to make.
67
67
  const ALREADY_DONE_LABEL = 'already-done'
68
+ // Marks the Issue that tracks a repository's next release (joshuafolkken/kit#3360). An Issue whose
69
+ // change reaches a consumer only once published is linked to it as a `blocked_by` blocker
70
+ // (`josh issue:release`), so "merged but not yet released" is visible in the backlog. A run applies
71
+ // it; **`auto-ok` never rides on it** — `issue-auto-ok.ts` refuses the default for it on every path,
72
+ // since a release is Tier C — and only a person opts it into unattended execution.
73
+ const RELEASE_LABEL = 'release'
68
74
 
69
75
  // The four labels that mean an open issue must not be handed to a run as it stands: an `epic`
70
76
  // tracks a batch and is never run directly (its children are), `in-progress` is already claimed by
@@ -228,6 +234,11 @@ const WORKFLOW_LABELS: ReadonlyArray<LabelDefinition> = [
228
234
  color: 'e99695',
229
235
  description: 'Offered first in a backlogrun; only a person removes it',
230
236
  },
237
+ {
238
+ name: RELEASE_LABEL,
239
+ color: 'fef2c0',
240
+ description: 'Tracks the next release; its blockers are the merged work it publishes',
241
+ },
231
242
  ...FILING_ROUTE_LABELS,
232
243
  ...DEPTH_LABELS,
233
244
  ]
@@ -340,6 +351,7 @@ const ALL_LABELS: ReadonlySet<string> = new Set([
340
351
  NEEDS_HUMAN_REVIEW_LABEL,
341
352
  ALREADY_DONE_LABEL,
342
353
  PRIORITY_HIGH_LABEL,
354
+ RELEASE_LABEL,
343
355
  INTERRUPT_ROUTE_LABEL,
344
356
  REVIEW_CAP_ROUTE_LABEL,
345
357
  REVIEW_ROUND2_SKIPPED_LABEL,
@@ -386,6 +398,7 @@ export {
386
398
  PRIORITY_HIGH_LABEL,
387
399
  RELEASE_CLASSIFICATION_LABELS,
388
400
  RELEASE_CLASSIFICATION_NAMES,
401
+ RELEASE_LABEL,
389
402
  REPOSITORY_LABELS,
390
403
  REVIEW_CAP_ROUTE_LABEL,
391
404
  REVIEW_ROUND2_SKIPPED_LABEL,
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
4
+ import { git_gh_exec } from '#scripts/gh/git-gh-exec'
5
+ import { git_gh_issue_list, MAX_SCANNED } from '#scripts/gh/git-gh-issue-list'
6
+ import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
7
+ import { github_issue_url } from '#scripts/gh/github-issue-url'
8
+ import { cli_flags } from '#scripts/lib/cli-flags'
9
+ import { error_text } from '#scripts/lib/error-message'
10
+ import { repository_labels } from '#scripts/repo/repository-labels'
11
+ import { RELEASE_LABEL } from './issue-labels'
12
+ import { issue_release } from './issue-release'
13
+
14
+ // `josh issue:release <N>` — link Issue `<N>` to its repository's release Issue as a `blocked_by`
15
+ // blocker, filing the release Issue when none is open (joshuafolkken/kit#3360). `josh issue:file
16
+ // --release` calls the same `link` once its Issue exists, so a new and an existing Issue are linked
17
+ // by one body of code.
18
+ //
19
+ // The relation is written through `issue_add_blocked_by`, which addresses the repository `gh`
20
+ // resolves — the current one, or the target a cross-repository `issue:file` has pointed `GH_REPO` at.
21
+
22
+ const SUCCESS_EXIT_CODE = 0
23
+ const FAILURE_EXIT_CODE = 1
24
+ const ARGV_OFFSET = 2
25
+ const ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/u
26
+ const USAGE = 'Usage: josh issue:release <issue-number>'
27
+ const NONE = 'none'
28
+ const UNKNOWN_REPO_MESSAGE =
29
+ 'Could not read this repository from `git remote` — check `gh auth status`.'
30
+
31
+ interface ReleaseIssue {
32
+ issue_number: number
33
+ // Filed by this call, rather than found open.
34
+ is_filed: boolean
35
+ }
36
+
37
+ // `undefined` when the listing is unreadable, `NONE` when it reads and names no open release Issue.
38
+ async function find_open(target: string): Promise<number | typeof NONE | undefined> {
39
+ const outcome = await git_gh_issue_list.issue_list({
40
+ json_fields: 'number',
41
+ limit: MAX_SCANNED,
42
+ label: RELEASE_LABEL,
43
+ repo: target,
44
+ })
45
+
46
+ if (outcome.json === undefined) return undefined
47
+
48
+ return issue_release.open_release_of(outcome.json) ?? NONE
49
+ }
50
+
51
+ // The new release Issue's number, or `undefined` when the create call failed. Never `auto-ok`: the
52
+ // labels are `RELEASE_LABELS`, decided here rather than by `issue_auto_ok`, which refuses the label too.
53
+ async function file_release(target: string): Promise<number | undefined> {
54
+ repository_labels.ensure_labels(target)
55
+ const request = git_gh_issue_write.issue_create_request({
56
+ title: issue_release.title_of(target),
57
+ body: issue_release.body_of(target),
58
+ labels: issue_release.RELEASE_LABELS,
59
+ repo: target,
60
+ })
61
+
62
+ try {
63
+ const url = await git_gh_exec.exec_gh_api(request)
64
+ const issue_number = Number(github_issue_url.parse(url)?.issue_number) || undefined
65
+
66
+ if (issue_number === undefined) console.error(`✖ the release number is unreadable: ${url}`)
67
+
68
+ return issue_number
69
+ } catch (error) {
70
+ console.error(`✖ the release Issue could not be filed: ${error_text.message_of(error)}`)
71
+
72
+ return undefined
73
+ }
74
+ }
75
+
76
+ // An unreadable listing files nothing: a second release Issue beside one the listing failed to show
77
+ // would split the blockers between two releases.
78
+ async function release_of(target: string): Promise<ReleaseIssue | undefined> {
79
+ const open = await find_open(target)
80
+
81
+ if (open === undefined) {
82
+ console.error(`✖ the open ${RELEASE_LABEL} Issues of ${target} could not be listed`)
83
+
84
+ return undefined
85
+ }
86
+
87
+ if (open !== NONE) return { issue_number: open, is_filed: false }
88
+ const filed = await file_release(target)
89
+
90
+ return filed === undefined ? undefined : { issue_number: filed, is_filed: true }
91
+ }
92
+
93
+ function linked_line(release: ReleaseIssue, issue_number: number): string {
94
+ const how = release.is_filed ? 'filed' : 'open'
95
+
96
+ return `release: #${String(release.issue_number)} (${how}) is blocked by #${String(issue_number)}`
97
+ }
98
+
99
+ // Whether Issue `issue_number` of `target` now blocks the release Issue. Every outcome is printed.
100
+ async function link(issue_number: number, target: string): Promise<boolean> {
101
+ const release = await release_of(target)
102
+
103
+ if (release === undefined) return false
104
+ const blocked = String(release.issue_number)
105
+ const is_linked = await git_gh_issue_write.issue_add_blocked_by(blocked, String(issue_number))
106
+
107
+ if (is_linked) console.info(linked_line(release, issue_number))
108
+ else console.error(`✖ #${String(issue_number)} could not be recorded as a blocker of #${blocked}`)
109
+
110
+ return is_linked
111
+ }
112
+
113
+ // An unknown flag parses to no positionals, so it refuses the call like a missing number.
114
+ function positionals_of(argv: ReadonlyArray<string>): ReadonlyArray<string> {
115
+ return cli_flags.arguments_of(argv, {})?.positionals ?? []
116
+ }
117
+
118
+ // The number is the one positional: a missing, malformed or second one refuses the whole call.
119
+ function issue_number_of(argv: ReadonlyArray<string>): number | undefined {
120
+ const [issue_number, ...rest] = positionals_of(argv)
121
+ const is_single = rest.length === 0 && issue_number !== undefined
122
+
123
+ return is_single && ISSUE_NUMBER_PATTERN.test(issue_number) ? Number(issue_number) : undefined
124
+ }
125
+
126
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
127
+ const issue_number = issue_number_of(argv)
128
+
129
+ if (issue_number === undefined) {
130
+ console.error(USAGE)
131
+
132
+ return FAILURE_EXIT_CODE
133
+ }
134
+
135
+ const current = await git_gh_command.repo_get_name_with_owner()
136
+
137
+ if (current === undefined) {
138
+ console.error(UNKNOWN_REPO_MESSAGE)
139
+
140
+ return FAILURE_EXIT_CODE
141
+ }
142
+
143
+ return (await link(issue_number, current)) ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
144
+ }
145
+
146
+ // `process.exitCode` rather than `process.exit()`, so the report on standard output drains first.
147
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
148
+ process.exitCode = await run(argv)
149
+ }
150
+
151
+ const issue_release_cli = { link, run }
152
+
153
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
154
+
155
+ export { issue_release_cli }
@@ -0,0 +1,58 @@
1
+ import { DEPTH_0_LABEL, RELEASE_LABEL } from './issue-labels'
2
+
3
+ // The decisions behind `josh issue:release` (joshuafolkken/kit#3360), kept apart from the command
4
+ // that reads the network so each is a pure answer a test can pin. An Issue whose change reaches a
5
+ // consumer only once published used to surface the release only at the end of `followup`'s report;
6
+ // it is now linked to the repository's release Issue, which it blocks until it merges.
7
+
8
+ interface ListingRow {
9
+ number: number
10
+ }
11
+
12
+ function is_listing_row(value: unknown): value is ListingRow {
13
+ if (typeof value !== 'object' || value === null) return false
14
+
15
+ return Number.isSafeInteger(Reflect.get(value, 'number'))
16
+ }
17
+
18
+ // The open release Issue a listing of `release`-labelled Issues names, or `undefined` when it names
19
+ // none or is unreadable. One release publishes every merge since the last, so a repository holds one
20
+ // release Issue at a time; were a person to open a second, the oldest is the one the next release
21
+ // closes, so it is the one every new blocker joins.
22
+ function open_release_of(json: string): number | undefined {
23
+ try {
24
+ const parsed: unknown = JSON.parse(json)
25
+ const rows = Array.isArray(parsed) ? parsed.filter((row) => is_listing_row(row)) : []
26
+
27
+ return rows.length === 0 ? undefined : Math.min(...rows.map((row) => row.number))
28
+ } catch {
29
+ return undefined
30
+ }
31
+ }
32
+
33
+ function title_of(repo: string): string {
34
+ return `Release ${repo} (next version)`
35
+ }
36
+
37
+ // Plain English with no classification declaration: the Issue is not filed through `issue:file`, and
38
+ // its labels are `RELEASE_LABELS` rather than read off the body.
39
+ function body_of(repo: string): string {
40
+ return [
41
+ `Tracks the next release of ${repo}.`,
42
+ '',
43
+ 'Every Issue this one is blocked by changes something a consumer can use only once it is published. Once they have merged, a person runs `pnpm josh release` in the primary checkout.',
44
+ '',
45
+ 'Close this Issue once that release is published: the next blocker files a new one, so an open release Issue names only unreleased work.',
46
+ '',
47
+ '`auto-ok` is never applied to this Issue by a run; a person may apply it.',
48
+ '',
49
+ ].join('\n')
50
+ }
51
+
52
+ // The release label and a depth: the subject of a release is what a consumer touches.
53
+ // Never `auto-ok`.
54
+ const RELEASE_LABELS: ReadonlyArray<string> = [RELEASE_LABEL, DEPTH_0_LABEL]
55
+
56
+ const issue_release = { RELEASE_LABELS, open_release_of, title_of, body_of }
57
+
58
+ export { issue_release }
@@ -1,7 +1,7 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { issue_label_schema } from '#scripts/git/git-schemas'
2
3
  import { PAGE_CEILING_CAUSE } from '#scripts/git/listing-cutoff'
3
4
  import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
4
- import { issue_label_schema } from '#scripts/git/schemas'
5
5
  import { z } from 'zod'
6
6
  import { EPIC_LABEL, has_any_label } from './issue-labels'
7
7
  import type { ScoutIssue } from './issue-scout'
@@ -2,6 +2,7 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { git_gh_command } from '#scripts/gh/git-gh-command'
4
4
  import { bounded_pool } from '#scripts/lib/bounded-pool'
5
+ import { cli_flags } from '#scripts/lib/cli-flags'
5
6
  import { issue_report_failures, type ReadFailureKind } from './issue-report-failures'
6
7
  import { issue_state, type IssueState } from './issue-state'
7
8
 
@@ -28,9 +29,6 @@ import { issue_state, type IssueState } from './issue-state'
28
29
 
29
30
  const FAILURE_EXIT_CODE = 1
30
31
  const ARGV_OFFSET = 2
31
- const REPO_FLAG = '--repo'
32
- const FLAG_PREFIX = '-'
33
- const INLINE_REPO_FLAG = `${REPO_FLAG}=`
34
32
  const ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/u
35
33
  const USAGE = 'Usage: josh issue:state <issue-number> [<issue-number> ...] [--repo <owner/repo>]'
36
34
  // A blank line between the blocks of a multi-number report, so a person sees where one issue ends
@@ -60,85 +58,43 @@ interface IssueReport {
60
58
  result: StateRead
61
59
  }
62
60
 
63
- // `absent` and "given but with nothing usable after it" are different answers. Falling back to the
64
- // session's repository on the second would print a confident state for a *different* issue of the
65
- // same number, which is the exact misread the `--repo` argument exists to prevent.
66
- //
67
- // A named flag carries the argv positions it actually occupied, never the token text it matched
68
- // (joshuafolkken/kit#1355). Removing tokens by value removes every token that happens to look like
69
- // one of them — a second `--repo`, a repeated `owner/repo` — and a token removed is a token nobody
70
- // is told about, which is the whole failure this file refuses elsewhere.
71
- type RepoFlag =
72
- | { kind: 'absent' }
73
- | { kind: 'named'; repo: string; consumed: ReadonlyArray<number> }
74
- | { kind: 'incomplete' }
75
-
76
- // Both spellings gh itself accepts. `--repo=owner/repo` falling through to the separate-word branch
77
- // would read as no repository at all, which is the fall back to the session's repository above.
78
- function read_inline_repo(argv: ReadonlyArray<string>): RepoFlag | undefined {
79
- const inline = argv.find((argument) => argument.startsWith(INLINE_REPO_FLAG))
80
-
81
- if (inline === undefined) return undefined
82
-
83
- const repo = inline.slice(INLINE_REPO_FLAG.length)
84
-
85
- if (repo.length === 0) return { kind: 'incomplete' }
86
-
87
- return { kind: 'named', repo, consumed: [argv.indexOf(inline)] }
61
+ // The read goes through `cli_flags`, both spellings gh itself accepts included (`--repo owner/repo`,
62
+ // `--repo=owner/repo`), and is strict (joshuafolkken/kit#3261). An unrecognized flag refuses the
63
+ // call: `--rep=owner/repo` used to be discarded for starting with a dash, so the call fell back to
64
+ // the session's repository and printed a confident state for a *different* repository's issue of
65
+ // that number (joshuafolkken/kit#1355).
66
+ const OPTIONS = { repo: { type: 'string', multiple: true } } as const
67
+
68
+ // `absent` and "given but with nothing usable" are different answers. Falling back to the session's
69
+ // repository on the second — an empty value, or `--repo` given twice — would print a confident state
70
+ // for a *different* issue of the same number, which is the exact misread `--repo` exists to prevent.
71
+ function read_repo(given: ReadonlyArray<string> | undefined): { repo?: string } | undefined {
72
+ if (given === undefined) return {}
73
+ const [repo, ...rest] = given
74
+
75
+ return repo !== undefined && rest.length === 0 && repo.length > 0 ? { repo } : undefined
88
76
  }
89
77
 
90
- function read_separate_repo(argv: ReadonlyArray<string>): RepoFlag {
91
- const index = argv.indexOf(REPO_FLAG)
92
-
93
- if (index === -1) return { kind: 'absent' }
94
-
95
- const value_index = index + 1
96
- const repo = argv[value_index]
97
-
98
- if (repo === undefined || repo.startsWith(FLAG_PREFIX)) return { kind: 'incomplete' }
99
-
100
- return { kind: 'named', repo, consumed: [index, value_index] }
101
- }
102
-
103
- function read_repo_flag(argv: ReadonlyArray<string>): RepoFlag {
104
- return read_inline_repo(argv) ?? read_separate_repo(argv)
105
- }
106
-
107
- // The argv positions the repository flag occupied, and nothing else. Everything outside this set was
108
- // offered as an issue number, whatever it looks like — so a flag nobody recognizes reaches the check
109
- // below and refuses the call instead of vanishing from it (joshuafolkken/kit#1355).
110
- function consumed_indices(repo_flag: RepoFlag): ReadonlySet<number> {
111
- return new Set(repo_flag.kind === 'named' ? repo_flag.consumed : [])
78
+ // A token that is not a number refuses the whole invocation rather than being dropped. Dropping it
79
+ // answers fewer numbers than were asked for and still exits zero — and with one number left, the
80
+ // surviving block prints in the single-number shape, so nothing in the output says a number went
81
+ // unanswered. `#1262` copied out of a `diag` table is exactly that token.
82
+ function is_issue_numbers(positionals: ReadonlyArray<string>): boolean {
83
+ return (
84
+ positionals.length > 0 && positionals.every((argument) => ISSUE_NUMBER_PATTERN.test(argument))
85
+ )
112
86
  }
113
87
 
114
88
  function parse_request(argv: ReadonlyArray<string>): StateRequest | undefined {
115
- const repo_flag = read_repo_flag(argv)
116
-
117
- if (repo_flag.kind === 'incomplete') return undefined
118
-
119
- // The repository is read first, flag word and value together, so neither is ever mistaken for an
120
- // issue number — and nothing else is removed here. Every remaining token is one the caller offered
121
- // as an issue number, whatever it looks like.
122
- const consumed = consumed_indices(repo_flag)
123
- const rest = argv.filter((_argument, index) => !consumed.has(index))
124
- const numbers = rest.filter((argument) => ISSUE_NUMBER_PATTERN.test(argument))
125
-
126
- // A token that is not a number refuses the whole invocation rather than being dropped. Dropping
127
- // it answers fewer numbers than were asked for and still exits zero — and with one number left,
128
- // the surviving block prints in the single-number shape, so nothing in the output says a number
129
- // went unanswered. `#1262` copied out of a `diag` table is exactly that token.
130
- //
131
- // An unrecognized flag reaches this same check rather than a lenient one of its own
132
- // (joshuafolkken/kit#1355). `--rep=owner/repo` used to be discarded for starting with a dash, so
133
- // the call fell back to the session's repository and printed a confident state for a *different*
134
- // repository's issue of that number — the misread `incomplete` already exists to prevent.
135
- if (numbers.length === 0 || numbers.length !== rest.length) return undefined
89
+ const parsed = cli_flags.arguments_of(argv, OPTIONS)
90
+ if (parsed === undefined) return undefined
91
+ const repo = read_repo(parsed.values.repo)
92
+
93
+ if (repo === undefined || !is_issue_numbers(parsed.positionals)) return undefined
136
94
 
137
95
  // In the order they were typed, with a repeat dropped: repeating a number would spend a second
138
96
  // read on an answer already in hand, and print a second block a caller counting rows counts twice.
139
- const issue_numbers = [...new Set(numbers)]
140
-
141
- return repo_flag.kind === 'named' ? { issue_numbers, repo: repo_flag.repo } : { issue_numbers }
97
+ return { issue_numbers: [...new Set(parsed.positionals)], ...repo }
142
98
  }
143
99
 
144
100
  // One number's read, reduced to what the report needs and nothing printed yet. Separating the two
@@ -78,6 +78,8 @@ const ALIASES: Record<string, string> = {
78
78
  rvf: 'review:findings',
79
79
  ruv: 'rule:value',
80
80
  ev: 'eval',
81
+ lsm: 'lane:sample',
82
+ lst: 'lane:stats',
81
83
  }
82
84
 
83
85
  // The canonical name of a `josh` subcommand: an alias expands, and anything else passes through