@joshuafolkken/kit 1.1166.0 → 1.1229.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/settings.json +2 -2
  3. package/.claude/skills/dependency-update/SKILL.md +4 -8
  4. package/.claude/skills/epic-commands/SKILL.md +2 -2
  5. package/.claude/skills/epic-commands/epic-bundle.md +1 -1
  6. package/.claude/skills/workflow-commands/SKILL.md +2 -4
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +9 -5
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +29 -9
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -2
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +24 -15
  13. package/.claude/skills/workflow-commands/chain-rule.md +1 -1
  14. package/.claude/skills/workflow-commands/delegation.md +2 -2
  15. package/.claude/skills/workflow-commands/followup.md +5 -0
  16. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  18. package/.claude/skills/workflow-commands/issue-scout.md +1 -1
  19. package/.claude/skills/workflow-commands/kickoff.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +10 -5
  22. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  23. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  25. package/.codex/hooks.json +2 -2
  26. package/.cursorrules +6 -41
  27. package/.github/actions/setup-node/action.yml +1 -1
  28. package/.github/workflows/auto-tag.yml +3 -4
  29. package/.github/workflows/ci.yml +47 -33
  30. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  31. package/.github/workflows/publish.yml +48 -4
  32. package/.github/workflows/sonar-qube.yml +3 -0
  33. package/.pnpmfile.mjs +4 -3
  34. package/.prettierignore +8 -2
  35. package/CLAUDE.md +18 -20
  36. package/README.md +26 -19
  37. package/SECURITY.md +15 -1
  38. package/dist/CLAUDE.md +18 -20
  39. package/dist/commands/document-section-cli.js +734 -0
  40. package/dist/hooks/audit-provision.js +346 -0
  41. package/dist/hooks/batch-guard.js +5 -4
  42. package/dist/hooks/{chunk-DYAVAM6S.js → chunk-55HHG5PX.js} +1405 -631
  43. package/dist/hooks/{chunk-A6P7PN7P.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-5KTCMCOT.js → chunk-6HJJXRBX.js} +4 -2
  46. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  47. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  48. package/dist/hooks/{chunk-ENA7P5FB.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-ANJ2WGFD.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-GU5MATVE.js → chunk-JUDKDCDP.js} +499 -389
  52. package/dist/hooks/{chunk-ZDZ555YH.js → chunk-NV2JRZZO.js} +743 -909
  53. package/dist/hooks/{chunk-V2BTOZHT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-URJA6XY4.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-GYNGP7HI.js → chunk-VC3PZPZO.js} +1 -1
  56. package/dist/hooks/codex-hook-adapter.js +14 -12
  57. package/dist/hooks/duplicate-read-guard.js +6 -5
  58. package/dist/hooks/format-edited.js +4 -3
  59. package/dist/hooks/inputs.json +232 -210
  60. package/dist/hooks/investigation-guard.js +6 -5
  61. package/dist/hooks/pretool-guard.js +12 -10
  62. package/dist/hooks/session-lang.js +2 -4
  63. package/dist/hooks/stop-guard.js +81 -59
  64. package/dist/josh.js +872 -75
  65. package/dist/version/index.d.ts +7 -2
  66. package/dist/version/index.js +152 -75
  67. package/eslint/base.js +23 -1
  68. package/eslint/rules/code-quality.js +3 -0
  69. package/eslint/rules/early-return-one-liner.js +98 -0
  70. package/eslint/rules/unicorn.js +16 -1
  71. package/package.json +7 -6
  72. package/pnpm-workspace.yaml +9 -4
  73. package/prettier/basic.d.ts +3 -1
  74. package/prettier/index.d.ts +3 -1
  75. package/prettier/static.d.ts +3 -1
  76. package/prompts/coding-standards.md +1 -1
  77. package/prompts/collaboration-workflow/file-edits.md +4 -4
  78. package/prompts/collaboration-workflow/glossary.md +1 -1
  79. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  80. package/prompts/collaboration-workflow/overview.md +2 -2
  81. package/prompts/collaboration-workflow/principles.md +14 -12
  82. package/prompts/collaboration-workflow/report-format.md +1 -1
  83. package/prompts/collaboration-workflow/residency.md +1 -1
  84. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  85. package/prompts/collaboration-workflow/shell-body.md +6 -6
  86. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  87. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  88. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  89. package/prompts/review-rubric.md +1 -2
  90. package/prompts/review.md +1 -1
  91. package/scripts/agent/agent-headless.ts +29 -0
  92. package/scripts/agent/agent-launch-environment.ts +6 -2
  93. package/scripts/agent/codex-agent-argv.ts +2 -1
  94. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  95. package/scripts/backlog/backlog-arrival.ts +3 -3
  96. package/scripts/backlog/backlog-budget.ts +1 -1
  97. package/scripts/backlog/backlog-drive-args.ts +135 -0
  98. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  99. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  100. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  101. package/scripts/backlog/backlog-drive-named.ts +3 -3
  102. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  103. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  104. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  105. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  106. package/scripts/backlog/backlog-drive.ts +1 -1
  107. package/scripts/backlog/backlog-next.ts +31 -9
  108. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  109. package/scripts/backlog/backlog-offer.ts +1 -1
  110. package/scripts/backlog/backlog-plan-cli.ts +7 -6
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-rank.ts +18 -9
  113. package/scripts/backlog/backlog-ready.ts +1 -1
  114. package/scripts/backlog/backlog-restructure.ts +152 -0
  115. package/scripts/backlog/backlog-scope.ts +1 -1
  116. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  117. package/scripts/backlog/backlog-stalled.ts +1 -1
  118. package/scripts/backlog/backlog-waves.ts +15 -17
  119. package/scripts/build/build-codex-hooks.ts +3 -1
  120. package/scripts/build/build-commands.ts +66 -0
  121. package/scripts/build/build-hooks.ts +2 -1
  122. package/scripts/build/build-library.ts +12 -3
  123. package/scripts/build/build.ts +70 -0
  124. package/scripts/build/local-bin.ts +3 -13
  125. package/scripts/cost-runtime/context-cut-threshold.ts +1 -9
  126. package/scripts/cost-runtime/cost-verdict.ts +15 -28
  127. package/scripts/delegation/investigation-reads.ts +3 -1
  128. package/scripts/discovery/repo-origin.ts +5 -5
  129. package/scripts/doctor/doctor-consumer.ts +2 -2
  130. package/scripts/doctor/doctor-io.ts +2 -3
  131. package/scripts/document/capped-print-part.ts +30 -0
  132. package/scripts/document/capped-print.ts +3 -4
  133. package/scripts/document/document-byte-budget.ts +11 -2
  134. package/scripts/document/document-scan.ts +117 -0
  135. package/scripts/document/document-section.ts +143 -16
  136. package/scripts/document/entry-read-budget.ts +5 -3
  137. package/scripts/document/entry-read-set.ts +10 -89
  138. package/scripts/document/read-set-cost.ts +98 -0
  139. package/scripts/epic/epic-add-body.ts +1 -0
  140. package/scripts/epic/epic-audit-checks.ts +1 -1
  141. package/scripts/epic/epic-audit-rationale.ts +4 -3
  142. package/scripts/epic/epic-bundle-cli.ts +14 -3
  143. package/scripts/epic/epic-bundle-poll.ts +41 -0
  144. package/scripts/epic/epic-busy.ts +1 -1
  145. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  146. package/scripts/epic/epic-classify.ts +1 -1
  147. package/scripts/epic/epic-cli-argv.ts +78 -46
  148. package/scripts/epic/epic-cli-remove.ts +11 -19
  149. package/scripts/epic/epic-cli.ts +74 -73
  150. package/scripts/epic/epic-close-comment.ts +1 -1
  151. package/scripts/epic/epic-close.ts +1 -1
  152. package/scripts/epic/epic-cross-repo.ts +5 -5
  153. package/scripts/epic/epic-graph.ts +1 -1
  154. package/scripts/epic/epic-index.ts +1 -1
  155. package/scripts/epic/epic-issue.ts +2 -6
  156. package/scripts/epic/epic-lane-offer.ts +2 -2
  157. package/scripts/epic/epic-next.ts +29 -42
  158. package/scripts/epic/epic-solo-stale.ts +1 -1
  159. package/scripts/epic/epic-triage.ts +8 -4
  160. package/scripts/epic/epic-validate.ts +1 -1
  161. package/scripts/followup/git-followup-finish.ts +2 -2
  162. package/scripts/followup/git-followup-label.ts +2 -2
  163. package/scripts/followup/git-followup-merged.ts +2 -2
  164. package/scripts/followup/git-followup-stages.ts +9 -0
  165. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  166. package/scripts/followup/git-pr-followup.ts +5 -130
  167. package/scripts/gate/core-budget.ts +69 -1
  168. package/scripts/gate/gate-plan.ts +47 -7
  169. package/scripts/gate/gate-skip.ts +38 -7
  170. package/scripts/gate/hook-gate-reuse.ts +12 -10
  171. package/scripts/gate/pre-commit-type-check.ts +4 -1
  172. package/scripts/gate/project-checks.ts +6 -9
  173. package/scripts/gate/type-check-step.ts +2 -3
  174. package/scripts/gate/verification-gate.ts +27 -8
  175. package/scripts/gh/git-gh-api-path.ts +2 -2
  176. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  177. package/scripts/gh/git-gh-issue-write.ts +53 -7
  178. package/scripts/gh/git-gh-issue.ts +5 -3
  179. package/scripts/gh/git-gh-pr-read.ts +68 -19
  180. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  181. package/scripts/gh/git-gh-pr.ts +5 -1
  182. package/scripts/gh/git-pr-ai-review.ts +1 -1
  183. package/scripts/gh/git-pr-checks-eval.ts +9 -3
  184. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  185. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  186. package/scripts/gh/git-pr-checks.ts +18 -6
  187. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  188. package/scripts/gh/git-pr-error.ts +1 -3
  189. package/scripts/gh/git-pr.ts +9 -13
  190. package/scripts/gh/github-issue-url.ts +2 -2
  191. package/scripts/git/constants.ts +12 -6
  192. package/scripts/git/git-command.ts +20 -218
  193. package/scripts/git/git-diff-reads.ts +225 -0
  194. package/scripts/git/git-error.ts +4 -12
  195. package/scripts/git/git-pre-push-hook.ts +139 -0
  196. package/scripts/git/git-prompt.ts +2 -6
  197. package/scripts/git/{schemas.ts → git-schemas.ts} +2 -0
  198. package/scripts/git/git-spawn-sync.ts +3 -1
  199. package/scripts/git/git-spawn.ts +1 -1
  200. package/scripts/git/git-staging.ts +4 -6
  201. package/scripts/git/git-status.ts +3 -9
  202. package/scripts/git/main-merge.ts +57 -21
  203. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  204. package/scripts/hooks/edited-cspell.ts +2 -4
  205. package/scripts/hooks/format-edited-file.ts +3 -12
  206. package/scripts/hooks/pre-push-unit.ts +31 -9
  207. package/scripts/hooks/reserved-run.ts +2 -2
  208. package/scripts/hooks/run-hook.sh +5 -4
  209. package/scripts/hooks/stop-guard.ts +5 -3
  210. package/scripts/init/init-actions.ts +0 -5
  211. package/scripts/init/init-ai-copy.ts +13 -2
  212. package/scripts/init/init-logic-json-merge.ts +19 -0
  213. package/scripts/init/init-logic-secretlint.ts +7 -7
  214. package/scripts/init/init-logic-workspace.ts +41 -2
  215. package/scripts/init/init-logic.ts +16 -26
  216. package/scripts/init/init.ts +5 -15
  217. package/scripts/init/initial-commit.ts +4 -4
  218. package/scripts/init/kit-development-versions.ts +40 -0
  219. package/scripts/init/kit-setup-state.ts +2 -2
  220. package/scripts/init/start-exec.ts +68 -19
  221. package/scripts/init/start-setup-pr.ts +9 -9
  222. package/scripts/init/start-steps.ts +6 -10
  223. package/scripts/issue/defect-rate-cli.ts +1 -1
  224. package/scripts/issue/git-next-issues.ts +1 -1
  225. package/scripts/issue/issue-auto-ok.ts +1 -1
  226. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  227. package/scripts/issue/issue-backlinks.ts +9 -9
  228. package/scripts/issue/issue-comment-cli.ts +19 -13
  229. package/scripts/issue/issue-file-cli.ts +45 -18
  230. package/scripts/issue/issue-file.ts +24 -1
  231. package/scripts/issue/issue-scout-closed.ts +1 -1
  232. package/scripts/issue/issue-state-cli.ts +30 -74
  233. package/scripts/josh/josh-command-map.ts +2 -0
  234. package/scripts/josh/josh-command-types.ts +11 -0
  235. package/scripts/josh/josh-commands-ai.ts +4 -4
  236. package/scripts/josh/josh-commands-backlog.ts +4 -1
  237. package/scripts/josh/josh-commands-development.ts +11 -1
  238. package/scripts/josh/josh-commands-document.ts +1 -0
  239. package/scripts/josh/josh-commands-lane.ts +22 -0
  240. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  241. package/scripts/josh/josh-commands-run.ts +12 -7
  242. package/scripts/josh/josh-in-process.ts +28 -9
  243. package/scripts/josh/josh-logic.ts +33 -15
  244. package/scripts/josh/process-identity.ts +1 -3
  245. package/scripts/josh/session-language-cli.ts +3 -4
  246. package/scripts/lane/lane-await.ts +5 -1
  247. package/scripts/lane/lane-child-invocation.ts +2 -2
  248. package/scripts/lane/lane-cli.ts +3 -3
  249. package/scripts/lane/lane-dispatch.ts +14 -10
  250. package/scripts/lane/lane-environment.ts +5 -1
  251. package/scripts/lane/lane-handoff.ts +1 -1
  252. package/scripts/lane/lane-install.ts +2 -13
  253. package/scripts/lane/lane-launch-cli.ts +35 -3
  254. package/scripts/lane/lane-ledger.ts +145 -0
  255. package/scripts/lane/lane-leftover.ts +1 -1
  256. package/scripts/lane/lane-load.ts +108 -0
  257. package/scripts/lane/lane-measure-cli.ts +149 -0
  258. package/scripts/lane/lane-occupancy.ts +3 -0
  259. package/scripts/lane/lane-reap.ts +1 -3
  260. package/scripts/lane/lane-relaunch.ts +1 -1
  261. package/scripts/lane/lane-stats.ts +224 -0
  262. package/scripts/lane/lane-vacant.ts +36 -0
  263. package/scripts/lane/openai-lane-ship.ts +2 -2
  264. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  265. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  266. package/scripts/lane/openai-review-broker.ts +2 -4
  267. package/scripts/lib/ancestor-directories.ts +35 -0
  268. package/scripts/lib/animation-helpers.ts +2 -2
  269. package/scripts/lib/buffered-process.ts +3 -7
  270. package/scripts/lib/cli-flags.ts +71 -1
  271. package/scripts/lib/spawn-exit.ts +2 -2
  272. package/scripts/lib/timeouts.ts +35 -2
  273. package/scripts/lines/line-budget.ts +4 -6
  274. package/scripts/notify/telegram-notify.ts +26 -1
  275. package/scripts/notify/telegram-test.ts +6 -0
  276. package/scripts/observations/observation-ledger-line.ts +6 -6
  277. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  278. package/scripts/observations/observation-ledger.ts +26 -7
  279. package/scripts/overrides/lockfile-overrides.ts +1 -1
  280. package/scripts/overrides/overrides-logic.ts +1 -1
  281. package/scripts/propagate/propagate-git.ts +1 -1
  282. package/scripts/propagate/propagate-publish.ts +2 -2
  283. package/scripts/propagate/propagate-steps.ts +20 -15
  284. package/scripts/propagate/propagate.ts +36 -27
  285. package/scripts/refactor/import-graph.ts +18 -1
  286. package/scripts/refactor/refactor-lint.ts +4 -4
  287. package/scripts/repo/repository-labels.ts +2 -2
  288. package/scripts/repo/required-checks-logic.ts +1 -3
  289. package/scripts/review/live-evidence.ts +2 -4
  290. package/scripts/review/review-brief-cli.ts +1 -1
  291. package/scripts/review/review-finding-ledger.ts +1 -1
  292. package/scripts/review/review-findings-cli.ts +1 -3
  293. package/scripts/rules/decision-oracle-batch.ts +128 -0
  294. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  295. package/scripts/rules/decision-oracle.ts +8 -120
  296. package/scripts/rules/delivered-rules.ts +26 -182
  297. package/scripts/rules/direct-filing.ts +1 -1
  298. package/scripts/rules/early-heartbeat.ts +3 -3
  299. package/scripts/rules/git-argv.ts +1 -1
  300. package/scripts/rules/git-force.ts +1 -1
  301. package/scripts/rules/implementation-cut.ts +2 -2
  302. package/scripts/rules/index-guard.ts +1 -1
  303. package/scripts/rules/issue-comments.ts +118 -0
  304. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  305. package/scripts/rules/lane-background.ts +2 -2
  306. package/scripts/rules/lane-park.ts +18 -0
  307. package/scripts/rules/lane-split-park.ts +128 -0
  308. package/scripts/rules/oracle-firing.ts +1 -1
  309. package/scripts/rules/poll-loop.ts +1 -1
  310. package/scripts/rules/pre-gate-cut.ts +3 -3
  311. package/scripts/rules/protected-files.ts +59 -4
  312. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  313. package/scripts/rules/shell-body-trigger.ts +1 -1
  314. package/scripts/rules/stop-rules.ts +18 -7
  315. package/scripts/rules/test-declared-commit.ts +4 -4
  316. package/scripts/rules/wip-cap.ts +63 -0
  317. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  318. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  319. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  320. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +12 -6
  321. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  322. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +3 -3
  323. package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +2 -2
  324. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  325. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  326. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  327. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  328. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  329. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  330. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  331. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  332. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  333. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  334. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  335. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  336. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  337. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  338. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  339. package/scripts/run/run-ending.ts +2 -2
  340. package/scripts/run/run-halfrun-resume.ts +6 -1
  341. package/scripts/run/run-headless.ts +1 -1
  342. package/scripts/run/run-invocation.ts +85 -84
  343. package/scripts/run/run-liveness.ts +1 -1
  344. package/scripts/run/run-parent-cut-hook.ts +1 -1
  345. package/scripts/run/run-preflight.ts +6 -6
  346. package/scripts/run/run-prep-cli.ts +1 -1
  347. package/scripts/run/run-prrun-resume.ts +1 -1
  348. package/scripts/run/run-report-cli.ts +4 -4
  349. package/scripts/run/run-report.ts +2 -2
  350. package/scripts/run/run-status-cli.ts +1 -1
  351. package/scripts/run/run-step-cli.ts +3 -3
  352. package/scripts/run/run-step.ts +2 -2
  353. package/scripts/run/run-stop-notify.ts +1 -1
  354. package/scripts/run/run-stranded-detect.ts +4 -4
  355. package/scripts/run/run-stranded.ts +2 -2
  356. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  357. package/scripts/run/run-watcher-guard.ts +5 -3
  358. package/scripts/run/run-watcher-hook.ts +1 -1
  359. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +29 -8
  360. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  361. package/scripts/run/ship/run-ship-lock.ts +50 -0
  362. package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +13 -0
  363. package/scripts/run/ship/run-ship-pre-detach.ts +66 -0
  364. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  365. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  366. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +10 -6
  367. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  368. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  369. package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +13 -0
  370. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +15 -8
  371. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  372. package/scripts/run/ship/run-ship-sync.ts +128 -0
  373. package/scripts/run/{run-ship.ts → ship/run-ship.ts} +6 -0
  374. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  375. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +31 -9
  376. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  377. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +99 -17
  378. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  379. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  380. package/scripts/run/wake/run-wake-restart.ts +58 -0
  381. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  382. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  383. package/scripts/safe-chain/preinstall-command.ts +20 -21
  384. package/scripts/safe-chain/project-config.ts +19 -15
  385. package/scripts/sync/sync-ai-files.ts +1 -1
  386. package/scripts/sync/sync-configs.ts +2 -1
  387. package/scripts/sync/sync.ts +15 -2
  388. package/scripts/test/test-red-logic.ts +17 -2
  389. package/scripts/test/test-red.ts +8 -7
  390. package/scripts/test/unit-worker-share.ts +1 -0
  391. package/scripts/time-runtime/time-background.ts +5 -1
  392. package/scripts/time-runtime/time-instant.ts +3 -1
  393. package/scripts/time-runtime/time-spans.ts +6 -139
  394. package/scripts/time-runtime/time-tool-call.ts +160 -0
  395. package/scripts/time-runtime/time-transcript-line.ts +27 -1
  396. package/scripts/version/effective-upstream.ts +2 -20
  397. package/scripts/version/latest-corepack.ts +17 -17
  398. package/scripts/version/latest-regression.ts +1 -1
  399. package/scripts/version/latest-update.ts +4 -4
  400. package/scripts/version/npm-registry.ts +20 -5
  401. package/scripts/version/publishable-range-check.ts +2 -2
  402. package/scripts/version/publishable-range.ts +1 -1
  403. package/scripts/version/release-age.ts +48 -38
  404. package/scripts/version/release-hold.ts +1 -1
  405. package/scripts/version/safe-chain-version-update.ts +40 -0
  406. package/scripts/version/upgrade-shell-command.ts +48 -8
  407. package/scripts/version/version-check-logic.ts +23 -14
  408. package/scripts/version/version-commands.ts +23 -11
  409. package/scripts/version/version-remote.ts +28 -13
  410. package/templates/pnpm-workspace.basic.yaml +4 -0
  411. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  412. package/templates/workflows/github-release.yml +2 -0
  413. package/scripts/build/build-config-merge.ts +0 -27
  414. package/scripts/build/build-managed-marker.ts +0 -27
  415. package/scripts/build/build-self-sync-guard.ts +0 -27
  416. package/scripts/build/build-version.ts +0 -26
  417. package/scripts/version/preinstall-version-update.ts +0 -86
  418. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  419. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  420. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  421. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  422. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  423. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  424. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  425. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  426. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  427. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  428. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  429. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  430. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  431. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  432. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  433. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  434. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  435. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  436. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  437. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -4,6 +4,7 @@ import { git_gh_command } from '#scripts/gh/git-gh-command'
4
4
  import { cutoff_of, type ScanCutoff } from '#scripts/git/listing-cutoff'
5
5
  import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
6
6
  import { bounded_pool } from '#scripts/lib/bounded-pool'
7
+ import type { PollOptions } from '#scripts/lib/poll'
7
8
  import { issue_citation } from '#scripts/rules/issue-citation'
8
9
  import { z } from 'zod'
9
10
  import {
@@ -14,6 +15,7 @@ import {
14
15
  } from './epic-bundle'
15
16
  import { epic_bundle_evidence } from './epic-bundle-evidence'
16
17
  import { epic_bundle_gaps } from './epic-bundle-gaps'
18
+ import { epic_bundle_poll } from './epic-bundle-poll'
17
19
  import { epic_bundle_referenced, type ReferencedContext } from './epic-bundle-referenced'
18
20
  import { epic_index, epic_schema, type FetchedEpics } from './epic-index'
19
21
  import { epic_issue } from './epic-issue'
@@ -393,9 +395,18 @@ async function report_widened(
393
395
  return report_decision(subject, widened, repo)
394
396
  }
395
397
 
396
- // The recommendation for one issue, from the open backlog around it.
397
- async function report_for(issue_number: number, repo: string): Promise<number> {
398
- const backlog = await read_backlog(repo)
398
+ // The recommendation for one issue, from the open backlog around it. `options` is the poll a caller
399
+ // holding a just-filed issue passes, since the listing trails the create call (`epic-bundle-poll.ts`).
400
+ async function report_for(
401
+ issue_number: number,
402
+ repo: string,
403
+ options?: PollOptions,
404
+ ): Promise<number> {
405
+ const backlog = await epic_bundle_poll.read_until_listed(
406
+ async () => await read_backlog(repo),
407
+ issue_number,
408
+ options,
409
+ )
399
410
 
400
411
  if (!backlog.is_readable) {
401
412
  console.error(unreadable_backlog_message(backlog))
@@ -0,0 +1,41 @@
1
+ import { poll, type PollOptions } from '#scripts/lib/poll'
2
+
3
+ // **The open listing trails the create call** (joshuafolkken/kit#3332): `issue:file` asks
4
+ // `epic:bundle` about the issue it has just filed, and the list endpoint had not shown it yet on every
5
+ // filing of one run, so the placement was skipped each time. A caller that knows the issue is fresh
6
+ // passes a poll and the listing is read again until it shows the issue. An unreadable listing is
7
+ // final, since reading it again is not what would change it.
8
+
9
+ // A number typed at the command line names an issue that exists already, so it is read once.
10
+ const SINGLE_READ: PollOptions = { attempts: 1, interval_ms: 0 }
11
+
12
+ interface Listing {
13
+ is_readable: boolean
14
+ issues: ReadonlyArray<{ number: number }>
15
+ }
16
+
17
+ function is_settled(listing: Listing, issue_number: number): boolean {
18
+ return !listing.is_readable || listing.issues.some((issue) => issue.number === issue_number)
19
+ }
20
+
21
+ async function read_until_listed<T extends Listing>(
22
+ read: () => Promise<T>,
23
+ issue_number: number,
24
+ options: PollOptions = SINGLE_READ,
25
+ ): Promise<T> {
26
+ const reads: Array<T> = []
27
+
28
+ await poll.poll_until(async () => {
29
+ const listing = await read()
30
+
31
+ reads.push(listing)
32
+
33
+ return is_settled(listing, issue_number)
34
+ }, options)
35
+
36
+ return reads.at(-1) ?? (await read())
37
+ }
38
+
39
+ const epic_bundle_poll = { read_until_listed }
40
+
41
+ export { epic_bundle_poll }
@@ -1,6 +1,6 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { open_issue_schema, type OpenIssueData } from '#scripts/git/git-schemas'
2
3
  import { read_json_listing } from '#scripts/git/parse-json-array'
3
- import { open_issue_schema, type OpenIssueData } from '#scripts/git/schemas'
4
4
  import {
5
5
  ALREADY_DONE_LABEL,
6
6
  has_any_label,
@@ -70,11 +70,12 @@ function is_same_blockers(
70
70
  left: ReadonlyArray<IssueReference>,
71
71
  right: ReadonlyArray<IssueReference>,
72
72
  ): boolean {
73
- const sorted = (blockers: ReadonlyArray<IssueReference>): string =>
74
- blockers
73
+ function sorted(blockers: ReadonlyArray<IssueReference>): string {
74
+ return blockers
75
75
  .map((blocker) => epic_graph.key_of(blocker))
76
76
  .toSorted((first, second) => first.localeCompare(second))
77
77
  .join(',')
78
+ }
78
79
 
79
80
  return sorted(left) === sorted(right)
80
81
  }
@@ -217,7 +217,7 @@ function is_ready(node: string, context: ClassifyContext): boolean {
217
217
  function fill_memo(children: ReadonlyArray<EpicChild>, context: ClassifyContext): void {
218
218
  const pending = new Set(children.map((child) => epic_graph.key_of(child)))
219
219
 
220
- const peel = (): number => {
220
+ function peel(): number {
221
221
  const ready = [...pending].filter((node) => is_ready(node, context))
222
222
 
223
223
  for (const node of ready) {
@@ -1,76 +1,108 @@
1
- // The argument-reading primitives every `josh epic` form shares.
1
+ import type { ParseArgsOptionsConfig } from 'node:util'
2
+ import { cli_flags } from '#scripts/lib/cli-flags'
3
+
4
+ // The argument reading every `josh epic` form shares, on top of `cli_flags` (joshuafolkken/kit#3261).
2
5
  //
3
- // They were private to `epic-cli.ts` until `--remove` needed the same four (joshuafolkken/kit#1712),
6
+ // They were private to `epic-cli.ts` until `--remove` needed the same readers (joshuafolkken/kit#1712),
4
7
  // at which point the choice was to copy them or to name them. Copying is what `CLAUDE.md` prohibits,
5
8
  // and the failure it would produce is specific: two readers disagreeing about what counts as a flag
6
9
  // value would have one form silently take a `--decision-file` path for an issue number.
10
+ //
11
+ // **Each form names its own flags, and the read is strict.** Which flags consume the argument after
12
+ // them is per form: `--before` takes a value only under `--add`, and treating it as one everywhere had
13
+ // `josh epic "T" 101 102 --after 103` silently drop #103 (joshuafolkken/kit#890). A flag the form does
14
+ // not know refuses the invocation — a mistyped flag would otherwise leave its value positional, so it
15
+ // becomes an issue number and the edit silently lands somewhere else.
7
16
 
8
- const FLAG_PREFIX = '--'
9
17
  const ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/u
18
+ // Every value flag is read as a list so a repeat is visible rather than resolved to one of the two:
19
+ // two `--decision-file` paths name two records, and two `--before` targets name two places.
20
+ const VALUE_OPTION = { type: 'string', multiple: true } as const
21
+ const SWITCH_OPTION = { type: 'boolean' } as const
22
+ const SINGLE = 1
23
+
24
+ type ParsedValue = string | boolean | Array<string | boolean> | undefined
25
+
26
+ interface EpicArgv {
27
+ positionals: Array<string>
28
+ values: Readonly<Record<string, ParsedValue>>
29
+ }
10
30
 
11
- function is_flag(argument: string): boolean {
12
- return argument.startsWith(FLAG_PREFIX)
31
+ interface FormFlags {
32
+ switches: ReadonlyArray<string>
33
+ value_flags?: ReadonlyArray<string>
13
34
  }
14
35
 
15
- function read_flag_value(argv: ReadonlyArray<string>, flag: string): string | undefined {
16
- const index = argv.indexOf(flag)
17
- if (index === -1) return undefined
36
+ type OptionEntry = [string, ParseArgsOptionsConfig[string]]
18
37
 
19
- return argv[index + 1]
38
+ function options_of(form: FormFlags): ParseArgsOptionsConfig {
39
+ const entries: Array<OptionEntry> = [
40
+ ...form.switches.map((flag): OptionEntry => [cli_flags.option_name(flag), SWITCH_OPTION]),
41
+ ...(form.value_flags ?? []).map((flag): OptionEntry => [
42
+ cli_flags.option_name(flag),
43
+ VALUE_OPTION,
44
+ ]),
45
+ ]
46
+
47
+ return Object.fromEntries(entries)
20
48
  }
21
49
 
22
- // A value-taking flag's argument must not be mistaken for a child issue number, so the argument
23
- // directly after one is dropped along with the flag itself.
24
- function is_flag_value(
25
- argv: ReadonlyArray<string>,
26
- index: number,
27
- value_flags: ReadonlySet<string>,
28
- ): boolean {
29
- return value_flags.has(argv[index - 1] ?? '')
50
+ // Every value a flag was given, in order; empty when it was absent.
51
+ function values_of_flag(parsed: EpicArgv, flag: string): ReadonlyArray<string> {
52
+ const given = parsed.values[cli_flags.option_name(flag)]
53
+ if (!Array.isArray(given)) return []
54
+
55
+ return given.filter((value): value is string => typeof value === 'string')
30
56
  }
31
57
 
32
- // **Which flags consume the argument after them is per parser, not module-wide**: `--before` takes a
33
- // value only under `--add`, and treating it as one everywhere had `josh epic "T" 101 102 --after 103`
34
- // silently drop #103 from the new epic instead of ignoring an unknown flag (joshuafolkken/kit#890).
35
- function to_positional_arguments(
36
- argv: ReadonlyArray<string>,
37
- value_flags: ReadonlySet<string>,
38
- ): Array<string> {
39
- return argv.filter(
40
- (argument, index) => !is_flag(argument) && !is_flag_value(argv, index, value_flags),
41
- )
58
+ function count_flag(parsed: EpicArgv, flag: string): number {
59
+ return values_of_flag(parsed, flag).length
42
60
  }
43
61
 
44
- function count_flag(argv: ReadonlyArray<string>, flag: string): number {
45
- return argv.filter((argument) => argument === flag).length
62
+ function is_repeated(parsed: EpicArgv, flag: string): boolean {
63
+ return count_flag(parsed, flag) > SINGLE
46
64
  }
47
65
 
48
- // A value-taking flag given without a usable value: last on the line, or followed by another flag.
49
- // **Refused rather than read as "none was asked for"** — `--decision-file` is passed precisely because
50
- // the record has to exist, so a shell that ate the path would otherwise land the edit, write no
51
- // record, post no comment and exit 0: success reported for half the job. Repeated, it names two
52
- // records, which is refused for the reason two positioning flags are (joshuafolkken/kit#1350).
53
- function is_value_unusable(argv: ReadonlyArray<string>, flag: string): boolean {
54
- if (!argv.includes(flag)) return false
55
- if (count_flag(argv, flag) > 1) return true
56
- const value = read_flag_value(argv, flag)
66
+ // The form's arguments, or `undefined` when a flag is unknown, a value is missing, or a value flag was
67
+ // given twice — a repeat is refused rather than silently taking one of the two.
68
+ function read_form(argv: ReadonlyArray<string>, form: FormFlags): EpicArgv | undefined {
69
+ const parsed = cli_flags.arguments_of(argv, options_of(form))
70
+ if (parsed === undefined) return undefined
57
71
 
58
- return value === undefined || is_flag(value)
72
+ return (form.value_flags ?? []).some((flag) => is_repeated(parsed, flag)) ? undefined : parsed
59
73
  }
60
74
 
61
- // `--add` and `--remove` refuse a flag they do not know, unlike creation and promotion which ignore
62
- // one. A typo there costs a flag; here a mistyped flag would leave its value positional, so it
63
- // becomes an issue number and the edit silently lands somewhere else (joshuafolkken/kit#890).
64
- function has_unknown_flag(argv: ReadonlyArray<string>, known: ReadonlySet<string>): boolean {
65
- return argv.some((argument) => is_flag(argument) && !known.has(argument))
75
+ // The form's positional arguments, or none when the read refused the invocation.
76
+ function positionals_of(argv: ReadonlyArray<string>, form: FormFlags): Array<string> {
77
+ return read_form(argv, form)?.positionals ?? []
78
+ }
79
+
80
+ function read_flag_value(parsed: EpicArgv, flag: string): string | undefined {
81
+ return values_of_flag(parsed, flag)[0]
82
+ }
83
+
84
+ function is_switch_set(parsed: EpicArgv, flag: string): boolean {
85
+ return parsed.values[cli_flags.option_name(flag)] === true
86
+ }
87
+
88
+ // A value-taking flag given without a usable value, or given twice. **Refused rather than read as "none
89
+ // was asked for"** — `--decision-file` is passed precisely because the record has to exist, so a shell
90
+ // that ate the path would otherwise land the edit, write no record, post no comment and exit 0: success
91
+ // reported for half the job. Repeated, it names two records (joshuafolkken/kit#1350).
92
+ function is_value_unusable(argv: ReadonlyArray<string>, flag: string): boolean {
93
+ return (
94
+ cli_flags.given_values(argv, flag).length > SINGLE || cli_flags.is_value_unusable(argv, flag)
95
+ )
66
96
  }
67
97
 
68
98
  const epic_cli_argv = {
99
+ read_form,
100
+ positionals_of,
69
101
  read_flag_value,
70
- to_positional_arguments,
102
+ is_switch_set,
71
103
  count_flag,
72
104
  is_value_unusable,
73
- has_unknown_flag,
74
105
  }
75
106
 
76
107
  export { epic_cli_argv, ISSUE_NUMBER_PATTERN }
108
+ export type { EpicArgv, FormFlags }
@@ -1,4 +1,4 @@
1
- import { epic_cli_argv, ISSUE_NUMBER_PATTERN } from './epic-cli-argv'
1
+ import { epic_cli_argv, ISSUE_NUMBER_PATTERN, type EpicArgv, type FormFlags } from './epic-cli-argv'
2
2
 
3
3
  // `josh epic --remove <E> <M> <N> …`'s argument rules (joshuafolkken/kit#1712).
4
4
  //
@@ -10,8 +10,7 @@ const REMOVE_FLAG = '--remove'
10
10
  const DECISION_FLAG = '--decision-file'
11
11
  // `--remove` takes no positioning flag: there is nothing for a removal to be relative to, and the
12
12
  // path itself says which orders go.
13
- const REMOVE_VALUE_FLAGS: ReadonlySet<string> = new Set([DECISION_FLAG])
14
- const REMOVE_KNOWN_FLAGS: ReadonlySet<string> = new Set([REMOVE_FLAG, DECISION_FLAG])
13
+ const REMOVE_FORM: FormFlags = { switches: [REMOVE_FLAG], value_flags: [DECISION_FLAG] }
15
14
  // The shortest path that names an order at all: `<M> <N>`.
16
15
  const MINIMUM_PATH_LENGTH = 2
17
16
 
@@ -36,30 +35,23 @@ function to_path_numbers(raw_path: ReadonlyArray<string>): Array<number> | undef
36
35
  return raw_path.length < MINIMUM_PATH_LENGTH ? undefined : raw_path.map(Number)
37
36
  }
38
37
 
39
- // A flag that makes the whole invocation unreadable, whatever the positional arguments say.
40
- function has_flag_refusal(argv: ReadonlyArray<string>): boolean {
41
- return (
42
- epic_cli_argv.has_unknown_flag(argv, REMOVE_KNOWN_FLAGS) ||
43
- epic_cli_argv.is_value_unusable(argv, DECISION_FLAG)
44
- )
45
- }
46
-
47
- function read_subject(
48
- argv: ReadonlyArray<string>,
49
- ): { epic_number: number; path: Array<number> } | undefined {
50
- const [raw_epic, ...raw_path] = epic_cli_argv.to_positional_arguments(argv, REMOVE_VALUE_FLAGS)
38
+ function read_subject(parsed: EpicArgv): { epic_number: number; path: Array<number> } | undefined {
39
+ const [raw_epic, ...raw_path] = parsed.positionals
51
40
  if (raw_epic === undefined || !ISSUE_NUMBER_PATTERN.test(raw_epic)) return undefined
52
41
  const path = to_path_numbers(raw_path)
53
42
 
54
43
  return path === undefined ? undefined : { epic_number: Number(raw_epic), path }
55
44
  }
56
45
 
46
+ // An unknown flag, or a `--decision-file` a shell ate, makes the whole invocation unreadable whatever
47
+ // the positional arguments say.
57
48
  function parse_remove_arguments(argv: ReadonlyArray<string>): RemoveArguments | undefined {
58
- if (has_flag_refusal(argv)) return undefined
59
- const subject = read_subject(argv)
60
- if (subject === undefined) return undefined
49
+ if (epic_cli_argv.is_value_unusable(argv, DECISION_FLAG)) return undefined
50
+ const parsed = epic_cli_argv.read_form(argv, REMOVE_FORM)
51
+ const subject = parsed === undefined ? undefined : read_subject(parsed)
52
+ if (parsed === undefined || subject === undefined) return undefined
61
53
 
62
- return { ...subject, decision_path: epic_cli_argv.read_flag_value(argv, DECISION_FLAG) }
54
+ return { ...subject, decision_path: epic_cli_argv.read_flag_value(parsed, DECISION_FLAG) }
63
55
  }
64
56
 
65
57
  const epic_cli_remove = {
@@ -1,6 +1,6 @@
1
1
  import { cli_body } from '#scripts/josh/cli-body'
2
2
  import type { InsertKind, InsertPosition } from './epic-chains'
3
- import { epic_cli_argv, ISSUE_NUMBER_PATTERN } from './epic-cli-argv'
3
+ import { epic_cli_argv, ISSUE_NUMBER_PATTERN, type EpicArgv, type FormFlags } from './epic-cli-argv'
4
4
  import { epic_cli_remove } from './epic-cli-remove'
5
5
  import { epic_parse, type ExternalChild } from './epic-parse'
6
6
 
@@ -27,25 +27,22 @@ const ORDER_AFTER_FLAG = '--order-after'
27
27
  // from a file, or from stdin as `-`. The text is a judgement, so the caller writes it; what the command
28
28
  // contributes is placing it in the epic's `## Decisions` and on each child (joshuafolkken/kit#1350).
29
29
  const DECISION_FLAG = '--decision-file'
30
- // Which flags consume the argument after them — per parser, for the reason `epic-cli-argv.ts` gives.
31
- const VALUE_FLAGS: ReadonlySet<string> = new Set([RATIONALE_FLAG, ORIGIN_FLAG])
32
- const ADD_VALUE_FLAGS: ReadonlySet<string> = new Set([
33
- BEFORE_FLAG,
34
- AFTER_FLAG,
35
- ORDER_BEFORE_FLAG,
36
- ORDER_AFTER_FLAG,
37
- DECISION_FLAG,
38
- ])
39
- const ADD_KNOWN_FLAGS: ReadonlySet<string> = new Set([
40
- ADD_FLAG,
41
- BEFORE_FLAG,
42
- AFTER_FLAG,
43
- ORDER_BEFORE_FLAG,
44
- ORDER_AFTER_FLAG,
45
- DECISION_FLAG,
46
- ])
30
+ // Which flags each form knows, and which of them consume the argument after them — per form, for the
31
+ // reason `epic-cli-argv.ts` gives.
32
+ const CREATE_FORM: FormFlags = {
33
+ switches: [ORDERED_FLAG],
34
+ value_flags: [RATIONALE_FLAG, ORIGIN_FLAG],
35
+ }
36
+ const PROMOTE_FORM: FormFlags = { ...CREATE_FORM, switches: [PROMOTE_FLAG, ORDERED_FLAG] }
37
+ const RECONCILE_FORM: FormFlags = { switches: [RECONCILE_FLAG] }
38
+ const CHECK_FORM: FormFlags = { switches: [] }
39
+ const ADD_FORM: FormFlags = {
40
+ switches: [ADD_FLAG],
41
+ value_flags: [BEFORE_FLAG, AFTER_FLAG, ORDER_BEFORE_FLAG, ORDER_AFTER_FLAG, DECISION_FLAG],
42
+ }
47
43
 
48
- const { count_flag, is_value_unusable, read_flag_value } = epic_cli_argv
44
+ const { count_flag, is_switch_set, is_value_unusable, positionals_of, read_flag_value, read_form } =
45
+ epic_cli_argv
49
46
 
50
47
  interface CreateArguments {
51
48
  title: string
@@ -65,13 +62,6 @@ interface PromoteArguments {
65
62
  origin?: string | undefined
66
63
  }
67
64
 
68
- function to_positional_arguments(
69
- argv: ReadonlyArray<string>,
70
- value_flags: ReadonlySet<string> = VALUE_FLAGS,
71
- ): Array<string> {
72
- return epic_cli_argv.to_positional_arguments(argv, value_flags)
73
- }
74
-
75
65
  // Deduplicated because a repeated number would render a duplicate task-list row, and with
76
66
  // `--ordered` would ask GitHub to make an issue block itself.
77
67
  function to_child_numbers(raw_children: ReadonlyArray<string>): Array<number> {
@@ -83,22 +73,33 @@ function to_child_numbers(raw_children: ReadonlyArray<string>): Array<number> {
83
73
  return [...new Set(numbers)]
84
74
  }
85
75
 
86
- // The title comes first so the children can stay a bare list of numbers, which is how they are
87
- // written down when the split is made.
88
- function parse_create_arguments(argv: ReadonlyArray<string>): CreateArguments | undefined {
89
- const [title, ...raw_children] = to_positional_arguments(argv)
76
+ // The flags a creation and a promotion share, read the same way for both.
77
+ function read_batch_flags(
78
+ parsed: EpicArgv,
79
+ ): Pick<CreateArguments, 'is_ordered' | 'rationale_path' | 'origin'> {
80
+ return {
81
+ is_ordered: is_switch_set(parsed, ORDERED_FLAG),
82
+ rationale_path: read_flag_value(parsed, RATIONALE_FLAG),
83
+ origin: read_flag_value(parsed, ORIGIN_FLAG),
84
+ }
85
+ }
86
+
87
+ function to_create_arguments(parsed: EpicArgv): CreateArguments | undefined {
88
+ const [title, ...raw_children] = parsed.positionals
90
89
  if (title === undefined || title.length === 0) return undefined
91
90
 
92
91
  const children = to_child_numbers(raw_children)
93
92
  if (children.length === 0) return undefined
94
93
 
95
- return {
96
- title,
97
- children,
98
- is_ordered: argv.includes(ORDERED_FLAG),
99
- rationale_path: read_flag_value(argv, RATIONALE_FLAG),
100
- origin: read_flag_value(argv, ORIGIN_FLAG),
101
- }
94
+ return { title, children, ...read_batch_flags(parsed) }
95
+ }
96
+
97
+ // The title comes first so the children can stay a bare list of numbers, which is how they are
98
+ // written down when the split is made.
99
+ function parse_create_arguments(argv: ReadonlyArray<string>): CreateArguments | undefined {
100
+ const parsed = read_form(argv, CREATE_FORM)
101
+
102
+ return parsed === undefined ? undefined : to_create_arguments(parsed)
102
103
  }
103
104
 
104
105
  // Whether the invocation is a promotion rather than a creation.
@@ -118,7 +119,7 @@ interface ReconcileArguments {
118
119
  // The epic to reconcile. The number after `--reconcile` is the only subject; there are no children
119
120
  // and no position — the repair reads what the epic already records rather than being told an order.
120
121
  function parse_reconcile_arguments(argv: ReadonlyArray<string>): ReconcileArguments | undefined {
121
- const [raw_epic] = to_positional_arguments(argv)
122
+ const [raw_epic] = positionals_of(argv, RECONCILE_FORM)
122
123
  if (raw_epic === undefined || !ISSUE_NUMBER_PATTERN.test(raw_epic)) return undefined
123
124
 
124
125
  return { epic_number: Number(raw_epic) }
@@ -127,20 +128,20 @@ function parse_reconcile_arguments(argv: ReadonlyArray<string>): ReconcileArgume
127
128
  // The promoted issue and its children. The number after `--promote` is the epic; everything else
128
129
  // positional is a child. Refused when the epic would also be listed as its own child, which would
129
130
  // have it block itself under `--ordered`.
130
- function parse_promote_arguments(argv: ReadonlyArray<string>): PromoteArguments | undefined {
131
- const [raw_epic, ...raw_children] = to_positional_arguments(argv)
131
+ function to_promote_arguments(parsed: EpicArgv): PromoteArguments | undefined {
132
+ const [raw_epic, ...raw_children] = parsed.positionals
132
133
  if (raw_epic === undefined || !ISSUE_NUMBER_PATTERN.test(raw_epic)) return undefined
133
134
  const epic_number = Number(raw_epic)
134
135
  const children = to_child_numbers(raw_children).filter((child) => child !== epic_number)
135
136
  if (children.length === 0) return undefined
136
137
 
137
- return {
138
- epic_number,
139
- children,
140
- is_ordered: argv.includes(ORDERED_FLAG),
141
- rationale_path: read_flag_value(argv, RATIONALE_FLAG),
142
- origin: read_flag_value(argv, ORIGIN_FLAG),
143
- }
138
+ return { epic_number, children, ...read_batch_flags(parsed) }
139
+ }
140
+
141
+ function parse_promote_arguments(argv: ReadonlyArray<string>): PromoteArguments | undefined {
142
+ const parsed = read_form(argv, PROMOTE_FORM)
143
+
144
+ return parsed === undefined ? undefined : to_promote_arguments(parsed)
144
145
  }
145
146
 
146
147
  // `--add <E> <N...> [--before <M> | --after <M> | --order-before <M> | --order-after <M>]`: the epic
@@ -195,36 +196,33 @@ const POSITION_FLAGS: ReadonlyArray<PositionFlag> = [
195
196
  // of each direction, or a dependency-writing flag beside an order-only one. `read_flag_value` would
196
197
  // answer with the first, which is a silent choice rather than a refusal — and here the silent choice
197
198
  // would decide whether a `blocked-by` is written at all.
198
- function is_position_ambiguous(argv: ReadonlyArray<string>): boolean {
199
- return POSITION_FLAGS.reduce((total, entry) => total + count_flag(argv, entry.flag), 0) > 1
199
+ function count_position_flags(parsed: EpicArgv): number {
200
+ return POSITION_FLAGS.reduce((total, entry) => total + count_flag(parsed, entry.flag), 0)
200
201
  }
201
202
 
202
203
  interface PositionTarget extends PositionFlag {
203
204
  raw: string
204
205
  }
205
206
 
206
- function to_position_target(
207
- argv: ReadonlyArray<string>,
208
- entry: PositionFlag,
209
- ): PositionTarget | undefined {
210
- const raw = read_flag_value(argv, entry.flag)
207
+ function to_position_target(parsed: EpicArgv, entry: PositionFlag): PositionTarget | undefined {
208
+ const raw = read_flag_value(parsed, entry.flag)
211
209
 
212
210
  return raw === undefined ? undefined : { ...entry, raw }
213
211
  }
214
212
 
215
- function read_position_target(argv: ReadonlyArray<string>): PositionTarget | undefined {
216
- if (is_position_ambiguous(argv)) return undefined
213
+ function read_position_target(parsed: EpicArgv): PositionTarget | undefined {
214
+ if (count_position_flags(parsed) > 1) return undefined
217
215
 
218
- return POSITION_FLAGS.map((entry) => to_position_target(argv, entry)).find(
216
+ return POSITION_FLAGS.map((entry) => to_position_target(parsed, entry)).find(
219
217
  (found) => found !== undefined,
220
218
  )
221
219
  }
222
220
 
223
221
  // A target that is not an issue number is refused for the same reason two flags are: guessing would
224
222
  // insert somewhere.
225
- function parse_position(argv: ReadonlyArray<string>): PositionOutcome {
226
- const has_flag = POSITION_FLAGS.some((entry) => argv.includes(entry.flag))
227
- const target = read_position_target(argv)
223
+ function parse_position(parsed: EpicArgv): PositionOutcome {
224
+ const has_flag = count_position_flags(parsed) > 0
225
+ const target = read_position_target(parsed)
228
226
  if (target === undefined) return has_flag ? REFUSED_POSITION : NO_POSITION
229
227
  if (!ISSUE_NUMBER_PATTERN.test(target.raw)) return REFUSED_POSITION
230
228
 
@@ -235,10 +233,6 @@ function parse_position(argv: ReadonlyArray<string>): PositionOutcome {
235
233
  }
236
234
  }
237
235
 
238
- function has_unknown_flag(argv: ReadonlyArray<string>): boolean {
239
- return epic_cli_argv.has_unknown_flag(argv, ADD_KNOWN_FLAGS)
240
- }
241
-
242
236
  // Whether *this* is why the insertion could not be read, so the refusal can say so. Without it the
243
237
  // caller prints the generic requirements line, and a shell that expanded `--decision-file $REC` to
244
238
  // nothing is answered with "an epic number and at least one child issue number are required" — advice
@@ -248,9 +242,9 @@ function is_decision_path_unusable(argv: ReadonlyArray<string>): boolean {
248
242
  }
249
243
 
250
244
  function read_add_subject(
251
- argv: ReadonlyArray<string>,
245
+ parsed: EpicArgv,
252
246
  ): { epic_number: number; children: Array<number> } | undefined {
253
- const [raw_epic, ...raw_children] = to_positional_arguments(argv, ADD_VALUE_FLAGS)
247
+ const [raw_epic, ...raw_children] = parsed.positionals
254
248
  if (raw_epic === undefined || !ISSUE_NUMBER_PATTERN.test(raw_epic)) return undefined
255
249
  const epic_number = Number(raw_epic)
256
250
  const children = to_child_numbers(raw_children).filter((child) => child !== epic_number)
@@ -258,21 +252,28 @@ function read_add_subject(
258
252
  return children.length === 0 ? undefined : { epic_number, children }
259
253
  }
260
254
 
261
- function parse_add_arguments(argv: ReadonlyArray<string>): AddArguments | undefined {
262
- if (has_unknown_flag(argv) || is_decision_path_unusable(argv)) return undefined
263
- const subject = read_add_subject(argv)
255
+ function to_add_arguments(parsed: EpicArgv): AddArguments | undefined {
256
+ const subject = read_add_subject(parsed)
264
257
  if (subject === undefined) return undefined
265
- const outcome = parse_position(argv)
258
+ const outcome = parse_position(parsed)
266
259
  if (outcome.is_refused) return undefined
267
260
 
268
261
  return {
269
262
  ...subject,
270
263
  position: outcome.position,
271
264
  is_order_only: outcome.is_order_only,
272
- decision_path: read_flag_value(argv, DECISION_FLAG),
265
+ decision_path: read_flag_value(parsed, DECISION_FLAG),
273
266
  }
274
267
  }
275
268
 
269
+ // An unknown flag is refused by the strict read: a mistyped positioning flag would otherwise leave its
270
+ // value positional, so it becomes a child and the edit lands somewhere else (joshuafolkken/kit#890).
271
+ function parse_add_arguments(argv: ReadonlyArray<string>): AddArguments | undefined {
272
+ const parsed = is_decision_path_unusable(argv) ? undefined : read_form(argv, ADD_FORM)
273
+
274
+ return parsed === undefined ? undefined : to_add_arguments(parsed)
275
+ }
276
+
276
277
  // The one refusal `--add` has to explain rather than merely report. `into owner/repo#N` is a legal
277
278
  // thing for a person to type, and this command cannot serve it: it reads and edits issues in the
278
279
  // repository it is run from. Falling through to the usage line would read as "that form does not
@@ -288,7 +289,7 @@ interface CrossRepoAddTarget {
288
289
  // problem, and
289
290
  // a suggestion built from it would drop or mangle what the person typed.
290
291
  function find_cross_repo_add_target(argv: ReadonlyArray<string>): CrossRepoAddTarget | undefined {
291
- const [raw_epic] = to_positional_arguments(argv, ADD_VALUE_FLAGS)
292
+ const [raw_epic] = positionals_of(argv, ADD_FORM)
292
293
  if (raw_epic === undefined) return undefined
293
294
  const epic = epic_parse.parse_external_reference(raw_epic)
294
295
  if (epic === undefined) return undefined
@@ -351,7 +352,7 @@ function format_cross_repo_refusal(found: CrossRepoAddTarget): string {
351
352
  }
352
353
 
353
354
  function parse_check_argument(argv: ReadonlyArray<string>): number | undefined {
354
- const [raw] = to_positional_arguments(argv)
355
+ const [raw] = positionals_of(argv, CHECK_FORM)
355
356
  if (raw === undefined || !ISSUE_NUMBER_PATTERN.test(raw)) return undefined
356
357
 
357
358
  return Number(raw)
@@ -1,6 +1,6 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { rest_comment_schema, type RestCommentData } from '#scripts/git/git-schemas'
2
3
  import { read_json_listing } from '#scripts/git/parse-json-array'
3
- import { rest_comment_schema, type RestCommentData } from '#scripts/git/schemas'
4
4
  import { epic_parse, type ExternalChild } from './epic-parse'
5
5
 
6
6
  // The comment the epic auto-close announces itself with, and how a run recognizes the one a previous
@@ -1,10 +1,10 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { epic_child_schema, epic_issue_schema, type EpicChildData } from '#scripts/git/git-schemas'
2
3
  import { cutoff_cause, cutoff_of, type ScanCutoff } from '#scripts/git/listing-cutoff'
3
4
  import {
4
5
  parse_json_array_or_undefined,
5
6
  parse_json_object_safe,
6
7
  } from '#scripts/git/parse-json-array'
7
- import { epic_child_schema, epic_issue_schema, type EpicChildData } from '#scripts/git/schemas'
8
8
  import { EPIC_LABEL } from '#scripts/issue/issue-labels'
9
9
  import { error_text } from '#scripts/lib/error-message'
10
10
  import { epic_close_comment } from './epic-close-comment'