@joshuafolkken/kit 1.1178.0 → 1.1229.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/settings.json +2 -2
  3. package/.claude/skills/dependency-update/SKILL.md +4 -8
  4. package/.claude/skills/epic-commands/SKILL.md +2 -2
  5. package/.claude/skills/epic-commands/epic-bundle.md +1 -1
  6. package/.claude/skills/workflow-commands/SKILL.md +2 -4
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -2
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +23 -2
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -2
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +4 -3
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +21 -13
  13. package/.claude/skills/workflow-commands/chain-rule.md +1 -1
  14. package/.claude/skills/workflow-commands/delegation.md +2 -2
  15. package/.claude/skills/workflow-commands/followup.md +5 -0
  16. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  18. package/.claude/skills/workflow-commands/issue-scout.md +1 -1
  19. package/.claude/skills/workflow-commands/kickoff.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-ledger.md +3 -3
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +6 -1
  22. package/.claude/skills/workflow-commands/prerequisite.md +2 -2
  23. package/.claude/skills/workflow-commands/progress-watcher.md +1 -1
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +1 -1
  25. package/.codex/hooks.json +2 -2
  26. package/.cursorrules +6 -41
  27. package/.github/actions/setup-node/action.yml +1 -1
  28. package/.github/workflows/auto-tag.yml +3 -4
  29. package/.github/workflows/ci.yml +32 -31
  30. package/.github/workflows/dependabot-auto-merge.yml +13 -6
  31. package/.github/workflows/publish.yml +48 -4
  32. package/.github/workflows/sonar-qube.yml +3 -0
  33. package/.pnpmfile.mjs +4 -3
  34. package/.prettierignore +8 -2
  35. package/CLAUDE.md +18 -20
  36. package/README.md +26 -19
  37. package/SECURITY.md +15 -1
  38. package/dist/CLAUDE.md +18 -20
  39. package/dist/commands/document-section-cli.js +734 -0
  40. package/dist/hooks/audit-provision.js +346 -0
  41. package/dist/hooks/batch-guard.js +5 -4
  42. package/dist/hooks/{chunk-S6X6WYKR.js → chunk-55HHG5PX.js} +1210 -711
  43. package/dist/hooks/{chunk-ZAL3NRGL.js → chunk-5VMKCCC4.js} +25 -10
  44. package/dist/hooks/chunk-5VNDSNG3.js +12 -0
  45. package/dist/hooks/{chunk-32GQTL4F.js → chunk-6HJJXRBX.js} +4 -2
  46. package/dist/hooks/chunk-CDY4NFQG.js +15 -0
  47. package/dist/hooks/chunk-ELCN5LCH.js +135 -0
  48. package/dist/hooks/{chunk-VESKRQOF.js → chunk-F56MFS4B.js} +3 -3
  49. package/dist/hooks/{chunk-UAGLLIGZ.js → chunk-FIVDHQGT.js} +7 -6
  50. package/dist/hooks/{chunk-3V37IID6.js → chunk-JBBMAPC4.js} +23 -14
  51. package/dist/hooks/{chunk-G6KRCKEC.js → chunk-JUDKDCDP.js} +496 -378
  52. package/dist/hooks/{chunk-GDS5N2KB.js → chunk-NV2JRZZO.js} +718 -681
  53. package/dist/hooks/{chunk-VQLLNFNT.js → chunk-RNWHN5BP.js} +3 -3
  54. package/dist/hooks/{chunk-DO6NWWHU.js → chunk-TQNPDZNH.js} +4 -4
  55. package/dist/hooks/{chunk-I4XSL4TT.js → chunk-VC3PZPZO.js} +1 -1
  56. package/dist/hooks/codex-hook-adapter.js +14 -12
  57. package/dist/hooks/duplicate-read-guard.js +6 -5
  58. package/dist/hooks/format-edited.js +4 -3
  59. package/dist/hooks/inputs.json +217 -195
  60. package/dist/hooks/investigation-guard.js +6 -5
  61. package/dist/hooks/pretool-guard.js +12 -10
  62. package/dist/hooks/session-lang.js +2 -4
  63. package/dist/hooks/stop-guard.js +66 -58
  64. package/dist/josh.js +868 -74
  65. package/dist/version/index.d.ts +7 -2
  66. package/dist/version/index.js +152 -75
  67. package/eslint/base.js +23 -1
  68. package/eslint/rules/code-quality.js +3 -0
  69. package/eslint/rules/early-return-one-liner.js +98 -0
  70. package/eslint/rules/unicorn.js +16 -1
  71. package/package.json +7 -6
  72. package/pnpm-workspace.yaml +9 -4
  73. package/prettier/basic.d.ts +3 -1
  74. package/prettier/index.d.ts +3 -1
  75. package/prettier/static.d.ts +3 -1
  76. package/prompts/coding-standards.md +1 -1
  77. package/prompts/collaboration-workflow/file-edits.md +4 -4
  78. package/prompts/collaboration-workflow/glossary.md +1 -1
  79. package/prompts/collaboration-workflow/operating-rules.md +17 -9
  80. package/prompts/collaboration-workflow/overview.md +2 -2
  81. package/prompts/collaboration-workflow/principles.md +14 -12
  82. package/prompts/collaboration-workflow/report-format.md +1 -1
  83. package/prompts/collaboration-workflow/residency.md +1 -1
  84. package/prompts/collaboration-workflow/rule-delivery.md +6 -6
  85. package/prompts/collaboration-workflow/shell-body.md +6 -6
  86. package/prompts/collaboration-workflow/turn-batching.md +1 -1
  87. package/prompts/collaboration-workflow/upstream-interrupt.md +12 -2
  88. package/prompts/collaboration-workflow/wip-cap.md +10 -10
  89. package/prompts/review-rubric.md +1 -2
  90. package/prompts/review.md +1 -1
  91. package/scripts/agent/agent-headless.ts +29 -0
  92. package/scripts/agent/agent-launch-environment.ts +6 -2
  93. package/scripts/agent/codex-agent-argv.ts +2 -1
  94. package/scripts/auto-ok/auto-ok-cli.ts +1 -1
  95. package/scripts/backlog/backlog-arrival.ts +3 -3
  96. package/scripts/backlog/backlog-budget.ts +1 -1
  97. package/scripts/backlog/backlog-drive-args.ts +135 -0
  98. package/scripts/backlog/backlog-drive-cli.ts +11 -137
  99. package/scripts/backlog/backlog-drive-launch.ts +39 -0
  100. package/scripts/backlog/backlog-drive-named-offer.ts +3 -5
  101. package/scripts/backlog/backlog-drive-named.ts +3 -3
  102. package/scripts/backlog/backlog-drive-offer-argv.ts +1 -1
  103. package/scripts/backlog/backlog-drive-owner.ts +1 -1
  104. package/scripts/backlog/backlog-drive-restore.ts +1 -1
  105. package/scripts/backlog/backlog-drive-retrospective.ts +1 -1
  106. package/scripts/backlog/backlog-drive.ts +1 -1
  107. package/scripts/backlog/backlog-next.ts +16 -5
  108. package/scripts/backlog/backlog-offer-cli.ts +6 -6
  109. package/scripts/backlog/backlog-offer.ts +1 -1
  110. package/scripts/backlog/backlog-plan-cli.ts +1 -1
  111. package/scripts/backlog/backlog-pool.ts +57 -11
  112. package/scripts/backlog/backlog-ready.ts +1 -1
  113. package/scripts/backlog/backlog-scope.ts +1 -1
  114. package/scripts/backlog/backlog-stalled-detect.ts +2 -2
  115. package/scripts/backlog/backlog-stalled.ts +1 -1
  116. package/scripts/build/build-codex-hooks.ts +3 -1
  117. package/scripts/build/build-commands.ts +66 -0
  118. package/scripts/build/build-hooks.ts +2 -1
  119. package/scripts/build/build-library.ts +12 -3
  120. package/scripts/build/build.ts +70 -0
  121. package/scripts/build/local-bin.ts +3 -13
  122. package/scripts/delegation/investigation-reads.ts +3 -1
  123. package/scripts/discovery/repo-origin.ts +5 -5
  124. package/scripts/doctor/doctor-consumer.ts +2 -2
  125. package/scripts/doctor/doctor-io.ts +2 -3
  126. package/scripts/document/capped-print-part.ts +30 -0
  127. package/scripts/document/capped-print.ts +3 -4
  128. package/scripts/document/document-byte-budget.ts +11 -2
  129. package/scripts/document/document-scan.ts +117 -0
  130. package/scripts/document/document-section.ts +143 -16
  131. package/scripts/document/entry-read-budget.ts +5 -3
  132. package/scripts/document/entry-read-set.ts +10 -89
  133. package/scripts/document/read-set-cost.ts +98 -0
  134. package/scripts/epic/epic-add-body.ts +1 -0
  135. package/scripts/epic/epic-audit-checks.ts +1 -1
  136. package/scripts/epic/epic-audit-rationale.ts +4 -3
  137. package/scripts/epic/epic-bundle-cli.ts +14 -3
  138. package/scripts/epic/epic-bundle-poll.ts +41 -0
  139. package/scripts/epic/epic-busy.ts +1 -1
  140. package/scripts/epic/epic-candidate-confirm.ts +3 -2
  141. package/scripts/epic/epic-classify.ts +1 -1
  142. package/scripts/epic/epic-cli-argv.ts +78 -46
  143. package/scripts/epic/epic-cli-remove.ts +11 -19
  144. package/scripts/epic/epic-cli.ts +74 -73
  145. package/scripts/epic/epic-close-comment.ts +1 -1
  146. package/scripts/epic/epic-close.ts +1 -1
  147. package/scripts/epic/epic-cross-repo.ts +5 -5
  148. package/scripts/epic/epic-graph.ts +1 -1
  149. package/scripts/epic/epic-index.ts +1 -1
  150. package/scripts/epic/epic-issue.ts +2 -6
  151. package/scripts/epic/epic-lane-offer.ts +2 -2
  152. package/scripts/epic/epic-next.ts +29 -42
  153. package/scripts/epic/epic-solo-stale.ts +1 -1
  154. package/scripts/epic/epic-triage.ts +8 -4
  155. package/scripts/epic/epic-validate.ts +1 -1
  156. package/scripts/followup/git-followup-finish.ts +2 -2
  157. package/scripts/followup/git-followup-label.ts +2 -2
  158. package/scripts/followup/git-followup-merged.ts +2 -2
  159. package/scripts/followup/git-pr-followup-checks.ts +142 -0
  160. package/scripts/followup/git-pr-followup.ts +4 -129
  161. package/scripts/gate/core-budget.ts +69 -1
  162. package/scripts/gate/gate-plan.ts +47 -7
  163. package/scripts/gate/gate-skip.ts +38 -7
  164. package/scripts/gate/hook-gate-reuse.ts +12 -10
  165. package/scripts/gate/pre-commit-type-check.ts +4 -1
  166. package/scripts/gate/project-checks.ts +6 -9
  167. package/scripts/gate/type-check-step.ts +2 -3
  168. package/scripts/gate/verification-gate.ts +27 -8
  169. package/scripts/gh/git-gh-api-path.ts +2 -2
  170. package/scripts/gh/git-gh-issue-rest.ts +3 -3
  171. package/scripts/gh/git-gh-issue-write.ts +53 -7
  172. package/scripts/gh/git-gh-pr-read.ts +64 -19
  173. package/scripts/gh/git-gh-pr-rest.ts +1 -1
  174. package/scripts/gh/git-gh-pr.ts +5 -1
  175. package/scripts/gh/git-pr-ai-review.ts +1 -1
  176. package/scripts/gh/git-pr-checks-eval.ts +1 -3
  177. package/scripts/gh/git-pr-checks-parse.ts +1 -1
  178. package/scripts/gh/git-pr-checks-watch.ts +6 -5
  179. package/scripts/gh/git-pr-checks.ts +7 -5
  180. package/scripts/gh/git-pr-coderabbit.ts +1 -1
  181. package/scripts/gh/git-pr-error.ts +1 -3
  182. package/scripts/gh/git-pr.ts +9 -13
  183. package/scripts/gh/github-issue-url.ts +2 -2
  184. package/scripts/git/constants.ts +2 -6
  185. package/scripts/git/git-command.ts +20 -218
  186. package/scripts/git/git-diff-reads.ts +225 -0
  187. package/scripts/git/git-error.ts +4 -12
  188. package/scripts/git/git-pre-push-hook.ts +139 -0
  189. package/scripts/git/git-prompt.ts +2 -6
  190. package/scripts/git/git-spawn-sync.ts +3 -1
  191. package/scripts/git/git-spawn.ts +1 -1
  192. package/scripts/git/git-staging.ts +4 -6
  193. package/scripts/git/git-status.ts +3 -9
  194. package/scripts/hooks/codex-hook-adapter.ts +1 -1
  195. package/scripts/hooks/edited-cspell.ts +2 -4
  196. package/scripts/hooks/format-edited-file.ts +3 -12
  197. package/scripts/hooks/pre-push-unit.ts +31 -9
  198. package/scripts/hooks/reserved-run.ts +2 -2
  199. package/scripts/hooks/run-hook.sh +5 -4
  200. package/scripts/hooks/stop-guard.ts +5 -3
  201. package/scripts/init/init-actions.ts +0 -5
  202. package/scripts/init/init-ai-copy.ts +13 -2
  203. package/scripts/init/init-logic-json-merge.ts +19 -0
  204. package/scripts/init/init-logic-secretlint.ts +7 -7
  205. package/scripts/init/init-logic-workspace.ts +41 -2
  206. package/scripts/init/init-logic.ts +16 -26
  207. package/scripts/init/init.ts +5 -15
  208. package/scripts/init/initial-commit.ts +4 -4
  209. package/scripts/init/kit-development-versions.ts +40 -0
  210. package/scripts/init/kit-setup-state.ts +2 -2
  211. package/scripts/init/start-exec.ts +68 -19
  212. package/scripts/init/start-setup-pr.ts +9 -9
  213. package/scripts/init/start-steps.ts +6 -10
  214. package/scripts/issue/defect-rate-cli.ts +1 -1
  215. package/scripts/issue/git-next-issues.ts +1 -1
  216. package/scripts/issue/issue-auto-ok.ts +1 -1
  217. package/scripts/issue/issue-backlinks-cli.ts +2 -2
  218. package/scripts/issue/issue-backlinks.ts +9 -9
  219. package/scripts/issue/issue-comment-cli.ts +19 -13
  220. package/scripts/issue/issue-file-cli.ts +45 -18
  221. package/scripts/issue/issue-file.ts +24 -1
  222. package/scripts/issue/issue-scout-closed.ts +1 -1
  223. package/scripts/issue/issue-state-cli.ts +30 -74
  224. package/scripts/josh/josh-command-map.ts +2 -0
  225. package/scripts/josh/josh-command-types.ts +11 -0
  226. package/scripts/josh/josh-commands-ai.ts +4 -4
  227. package/scripts/josh/josh-commands-backlog.ts +4 -1
  228. package/scripts/josh/josh-commands-development.ts +10 -0
  229. package/scripts/josh/josh-commands-document.ts +1 -0
  230. package/scripts/josh/josh-commands-lane.ts +22 -0
  231. package/scripts/josh/josh-commands-maintenance.ts +4 -4
  232. package/scripts/josh/josh-commands-run.ts +12 -7
  233. package/scripts/josh/josh-in-process.ts +28 -9
  234. package/scripts/josh/josh-logic.ts +33 -15
  235. package/scripts/josh/process-identity.ts +1 -3
  236. package/scripts/josh/session-language-cli.ts +3 -4
  237. package/scripts/lane/lane-await.ts +5 -1
  238. package/scripts/lane/lane-child-invocation.ts +2 -2
  239. package/scripts/lane/lane-cli.ts +3 -3
  240. package/scripts/lane/lane-dispatch.ts +14 -10
  241. package/scripts/lane/lane-environment.ts +5 -1
  242. package/scripts/lane/lane-handoff.ts +1 -1
  243. package/scripts/lane/lane-install.ts +2 -13
  244. package/scripts/lane/lane-launch-cli.ts +15 -1
  245. package/scripts/lane/lane-ledger.ts +145 -0
  246. package/scripts/lane/lane-leftover.ts +1 -1
  247. package/scripts/lane/lane-load.ts +108 -0
  248. package/scripts/lane/lane-measure-cli.ts +149 -0
  249. package/scripts/lane/lane-occupancy.ts +3 -0
  250. package/scripts/lane/lane-reap.ts +1 -3
  251. package/scripts/lane/lane-relaunch.ts +1 -1
  252. package/scripts/lane/lane-stats.ts +224 -0
  253. package/scripts/lane/lane-vacant.ts +36 -0
  254. package/scripts/lane/openai-lane-ship.ts +2 -2
  255. package/scripts/lane/openai-lane-supervisor-owner.ts +180 -0
  256. package/scripts/lane/openai-lane-supervisor.ts +7 -165
  257. package/scripts/lane/openai-review-broker.ts +2 -4
  258. package/scripts/lib/ancestor-directories.ts +35 -0
  259. package/scripts/lib/animation-helpers.ts +2 -2
  260. package/scripts/lib/buffered-process.ts +3 -7
  261. package/scripts/lib/cli-flags.ts +71 -1
  262. package/scripts/lib/spawn-exit.ts +2 -2
  263. package/scripts/lib/timeouts.ts +35 -2
  264. package/scripts/lines/line-budget.ts +4 -6
  265. package/scripts/notify/telegram-test.ts +6 -0
  266. package/scripts/observations/observation-ledger-line.ts +6 -6
  267. package/scripts/observations/observation-ledger-migrate.ts +55 -4
  268. package/scripts/observations/observation-ledger.ts +26 -7
  269. package/scripts/overrides/lockfile-overrides.ts +1 -1
  270. package/scripts/overrides/overrides-logic.ts +1 -1
  271. package/scripts/propagate/propagate-git.ts +1 -1
  272. package/scripts/propagate/propagate-publish.ts +2 -2
  273. package/scripts/propagate/propagate-steps.ts +20 -15
  274. package/scripts/propagate/propagate.ts +36 -27
  275. package/scripts/refactor/import-graph.ts +18 -1
  276. package/scripts/refactor/refactor-lint.ts +4 -4
  277. package/scripts/repo/repository-labels.ts +2 -2
  278. package/scripts/repo/required-checks-logic.ts +1 -3
  279. package/scripts/review/live-evidence.ts +2 -4
  280. package/scripts/review/review-brief-cli.ts +1 -1
  281. package/scripts/review/review-finding-ledger.ts +1 -1
  282. package/scripts/review/review-findings-cli.ts +1 -3
  283. package/scripts/rules/decision-oracle-batch.ts +128 -0
  284. package/scripts/rules/decision-oracle-tokens.ts +21 -0
  285. package/scripts/rules/decision-oracle.ts +8 -120
  286. package/scripts/rules/delivered-rules.ts +26 -182
  287. package/scripts/rules/direct-filing.ts +1 -1
  288. package/scripts/rules/early-heartbeat.ts +3 -3
  289. package/scripts/rules/git-argv.ts +1 -1
  290. package/scripts/rules/git-force.ts +1 -1
  291. package/scripts/rules/implementation-cut.ts +2 -2
  292. package/scripts/rules/index-guard.ts +1 -1
  293. package/scripts/rules/issue-comments.ts +118 -0
  294. package/scripts/rules/{issue-fold.ts → issue-fold-rule.ts} +2 -2
  295. package/scripts/rules/lane-background.ts +2 -2
  296. package/scripts/rules/lane-park.ts +18 -0
  297. package/scripts/rules/lane-split-park.ts +128 -0
  298. package/scripts/rules/oracle-firing.ts +1 -1
  299. package/scripts/rules/poll-loop.ts +1 -1
  300. package/scripts/rules/pre-gate-cut.ts +1 -1
  301. package/scripts/rules/protected-files.ts +59 -4
  302. package/scripts/rules/{run-tail.ts → run-tail-rule.ts} +2 -2
  303. package/scripts/rules/shell-body-trigger.ts +1 -1
  304. package/scripts/rules/stop-rules.ts +18 -7
  305. package/scripts/rules/test-declared-commit.ts +4 -4
  306. package/scripts/rules/wip-cap.ts +63 -0
  307. package/scripts/run/{run-carry-args.ts → carry/run-carry-args.ts} +37 -13
  308. package/scripts/run/{run-carry-change.ts → carry/run-carry-change.ts} +29 -7
  309. package/scripts/run/{run-carry-cli.ts → carry/run-carry-cli.ts} +4 -6
  310. package/scripts/run/{run-carry.ts → carry/run-carry.ts} +1 -1
  311. package/scripts/run/{run-cut-args.ts → cut/run-cut-args.ts} +1 -1
  312. package/scripts/run/{run-cut-cli.ts → cut/run-cut-cli.ts} +2 -2
  313. package/scripts/run/{run-cut.ts → cut/run-cut.ts} +1 -1
  314. package/scripts/run/{run-entry-cli.ts → entry/run-entry-cli.ts} +11 -11
  315. package/scripts/run/{run-entry.ts → entry/run-entry.ts} +2 -2
  316. package/scripts/run/{run-event-scope.ts → event/run-event-scope.ts} +1 -1
  317. package/scripts/run/{run-event-stream-emit.ts → event/run-event-stream-emit.ts} +1 -1
  318. package/scripts/run/{run-event-stream.ts → event/run-event-stream.ts} +16 -2
  319. package/scripts/run/{run-hold-cli.ts → hold/run-hold-cli.ts} +5 -5
  320. package/scripts/run/{run-merge-cli.ts → merge/run-merge-cli.ts} +10 -5
  321. package/scripts/run/{run-merge-steps.ts → merge/run-merge-steps.ts} +53 -5
  322. package/scripts/run/{run-merge.ts → merge/run-merge.ts} +1 -1
  323. package/scripts/run/{run-progress-cli.ts → progress/run-progress-cli.ts} +1 -1
  324. package/scripts/run/{run-progress-config.ts → progress/run-progress-config.ts} +2 -17
  325. package/scripts/run/{run-progress-read.ts → progress/run-progress-read.ts} +5 -5
  326. package/scripts/run/{run-progress.ts → progress/run-progress.ts} +1 -1
  327. package/scripts/run/{run-review-steps.ts → review/run-review-steps.ts} +2 -3
  328. package/scripts/run/run-ending.ts +2 -2
  329. package/scripts/run/run-halfrun-resume.ts +6 -1
  330. package/scripts/run/run-headless.ts +1 -1
  331. package/scripts/run/run-invocation.ts +85 -84
  332. package/scripts/run/run-liveness.ts +1 -1
  333. package/scripts/run/run-parent-cut-hook.ts +1 -1
  334. package/scripts/run/run-preflight.ts +6 -6
  335. package/scripts/run/run-prep-cli.ts +1 -1
  336. package/scripts/run/run-prrun-resume.ts +1 -1
  337. package/scripts/run/run-report-cli.ts +4 -4
  338. package/scripts/run/run-report.ts +2 -2
  339. package/scripts/run/run-status-cli.ts +1 -1
  340. package/scripts/run/run-step-cli.ts +3 -3
  341. package/scripts/run/run-step.ts +2 -2
  342. package/scripts/run/run-stop-notify.ts +1 -1
  343. package/scripts/run/run-stranded-detect.ts +4 -4
  344. package/scripts/run/run-stranded.ts +2 -2
  345. package/scripts/run/run-watcher-guard-cli.ts +1 -1
  346. package/scripts/run/run-watcher-guard.ts +5 -3
  347. package/scripts/run/run-watcher-hook.ts +1 -1
  348. package/scripts/run/{run-ship-cli.ts → ship/run-ship-cli.ts} +2 -2
  349. package/scripts/run/{run-ship-detach.ts → ship/run-ship-detach.ts} +3 -3
  350. package/scripts/run/ship/run-ship-lock.ts +50 -0
  351. package/scripts/run/{run-ship-preflight.ts → ship/run-ship-preflight.ts} +6 -2
  352. package/scripts/run/{run-ship-probe.ts → ship/run-ship-probe.ts} +2 -2
  353. package/scripts/run/{run-ship-return.ts → ship/run-ship-return.ts} +4 -3
  354. package/scripts/run/{run-ship-review-steps.ts → ship/run-ship-review-steps.ts} +3 -3
  355. package/scripts/run/{run-ship-scoped.ts → ship/run-ship-scoped.ts} +10 -1
  356. package/scripts/run/{run-ship-steps.ts → ship/run-ship-steps.ts} +11 -12
  357. package/scripts/run/ship/run-ship-stop-text.ts +13 -0
  358. package/scripts/run/{run-ship-sync.ts → ship/run-ship-sync.ts} +25 -11
  359. package/scripts/run/{run-tidy-lanes.ts → tidy/run-tidy-lanes.ts} +1 -1
  360. package/scripts/run/{run-wake-cli.ts → wake/run-wake-cli.ts} +23 -8
  361. package/scripts/run/{run-wake-describe.ts → wake/run-wake-describe.ts} +3 -3
  362. package/scripts/run/{run-wake-driver.ts → wake/run-wake-driver.ts} +59 -8
  363. package/scripts/run/{run-wake-handoff.ts → wake/run-wake-handoff.ts} +1 -1
  364. package/scripts/run/{run-wake-loop.ts → wake/run-wake-loop.ts} +8 -2
  365. package/scripts/run/wake/run-wake-restart.ts +58 -0
  366. package/scripts/run/{run-wake-session.ts → wake/run-wake-session.ts} +3 -3
  367. package/scripts/run/{run-wake.ts → wake/run-wake.ts} +1 -1
  368. package/scripts/safe-chain/preinstall-command.ts +20 -21
  369. package/scripts/safe-chain/project-config.ts +19 -15
  370. package/scripts/sync/sync-ai-files.ts +1 -1
  371. package/scripts/sync/sync-configs.ts +2 -1
  372. package/scripts/sync/sync.ts +15 -2
  373. package/scripts/test/unit-worker-share.ts +1 -0
  374. package/scripts/time-runtime/time-background.ts +5 -1
  375. package/scripts/time-runtime/time-instant.ts +3 -1
  376. package/scripts/time-runtime/time-spans.ts +6 -139
  377. package/scripts/time-runtime/time-tool-call.ts +160 -0
  378. package/scripts/version/effective-upstream.ts +2 -20
  379. package/scripts/version/latest-corepack.ts +17 -17
  380. package/scripts/version/latest-regression.ts +1 -1
  381. package/scripts/version/latest-update.ts +4 -4
  382. package/scripts/version/npm-registry.ts +20 -5
  383. package/scripts/version/publishable-range-check.ts +2 -2
  384. package/scripts/version/publishable-range.ts +1 -1
  385. package/scripts/version/release-age.ts +48 -38
  386. package/scripts/version/release-hold.ts +1 -1
  387. package/scripts/version/safe-chain-version-update.ts +40 -0
  388. package/scripts/version/upgrade-shell-command.ts +48 -8
  389. package/scripts/version/version-check-logic.ts +23 -14
  390. package/scripts/version/version-commands.ts +23 -11
  391. package/scripts/version/version-remote.ts +28 -13
  392. package/templates/pnpm-workspace.basic.yaml +4 -0
  393. package/templates/workflows/dependabot-auto-merge.yml +5 -5
  394. package/templates/workflows/github-release.yml +2 -0
  395. package/scripts/build/build-config-merge.ts +0 -27
  396. package/scripts/build/build-managed-marker.ts +0 -27
  397. package/scripts/build/build-self-sync-guard.ts +0 -27
  398. package/scripts/build/build-version.ts +0 -26
  399. package/scripts/version/preinstall-version-update.ts +0 -86
  400. /package/scripts/git/{schemas.ts → git-schemas.ts} +0 -0
  401. /package/scripts/overrides/{schemas.ts → overrides-schemas.ts} +0 -0
  402. /package/scripts/run/{run-carry-conversation.ts → carry/run-carry-conversation.ts} +0 -0
  403. /package/scripts/run/{run-carry-reclaim.ts → carry/run-carry-reclaim.ts} +0 -0
  404. /package/scripts/run/{run-carry-stash.ts → carry/run-carry-stash.ts} +0 -0
  405. /package/scripts/run/{run-carry-streak.ts → carry/run-carry-streak.ts} +0 -0
  406. /package/scripts/run/{run-cut-handoff.ts → cut/run-cut-handoff.ts} +0 -0
  407. /package/scripts/run/{run-cut-report.ts → cut/run-cut-report.ts} +0 -0
  408. /package/scripts/run/{run-entry-stop.ts → entry/run-entry-stop.ts} +0 -0
  409. /package/scripts/run/{run-event-cli.ts → event/run-event-cli.ts} +0 -0
  410. /package/scripts/run/{run-event-follow.ts → event/run-event-follow.ts} +0 -0
  411. /package/scripts/run/{run-event-render.ts → event/run-event-render.ts} +0 -0
  412. /package/scripts/run/{run-event-watch.ts → event/run-event-watch.ts} +0 -0
  413. /package/scripts/run/{run-hold.ts → hold/run-hold.ts} +0 -0
  414. /package/scripts/run/{run-progress-args.ts → progress/run-progress-args.ts} +0 -0
  415. /package/scripts/run/{run-progress-clock.ts → progress/run-progress-clock.ts} +0 -0
  416. /package/scripts/run/{run-review-cli.ts → review/run-review-cli.ts} +0 -0
  417. /package/scripts/run/{run-review.ts → review/run-review.ts} +0 -0
  418. /package/scripts/run/{run-ship-next.ts → ship/run-ship-next.ts} +0 -0
  419. /package/scripts/run/{run-ship-pre-detach.ts → ship/run-ship-pre-detach.ts} +0 -0
  420. /package/scripts/run/{run-ship-review.ts → ship/run-ship-review.ts} +0 -0
  421. /package/scripts/run/{run-ship-stage.ts → ship/run-ship-stage.ts} +0 -0
  422. /package/scripts/run/{run-ship.ts → ship/run-ship.ts} +0 -0
  423. /package/scripts/run/{run-tidy-cli.ts → tidy/run-tidy-cli.ts} +0 -0
  424. /package/scripts/run/{run-tidy-stashes.ts → tidy/run-tidy-stashes.ts} +0 -0
  425. /package/scripts/run/{run-tidy.ts → tidy/run-tidy.ts} +0 -0
@@ -14,20 +14,20 @@
14
14
 
15
15
  import { existsSync } from 'node:fs'
16
16
  import path from 'node:path'
17
- import { cost_tokens } from '#scripts/cost-runtime/cost-tokens'
18
17
  import { PACKAGE_DIR } from '#scripts/init/init-paths'
19
18
  import { bash_output_cap_reader } from './bash-output-cap'
20
- import { document_section, type Section } from './document-section'
19
+ import { document_section } from './document-section'
20
+ import { read_set_cost, type Cost } from './read-set-cost'
21
21
  import { read_set_point_of_use } from './read-set-point-of-use'
22
22
 
23
23
  const { POINT_OF_USE_FILES, POINT_OF_USE_BY_ENTRY, is_point_of_use, point_of_use_files } =
24
24
  read_set_point_of_use
25
+ const { cost_of, scoped_file_cost, total } = read_set_cost
25
26
 
26
27
  const SKILL_DIRECTORY = path.join('.claude', 'skills', 'workflow-commands')
27
28
  const SKILL_FILE = 'SKILL.md'
28
29
  const TABLE_SECTION = '1. Which file to read'
29
30
  const NOTHING = 0
30
- const ONE_LINE = 1
31
31
 
32
32
  // The point-of-use classification — which documents an entry names but reads only at a later step —
33
33
  // is its own concern, split into `read-set-point-of-use.ts` in joshuafolkken/kit#2289. This file
@@ -54,11 +54,6 @@ interface SectionReference {
54
54
  heading: string
55
55
  }
56
56
 
57
- interface Cost {
58
- bytes: number
59
- tokens: number
60
- }
61
-
62
57
  interface SectionCost extends SectionReference {
63
58
  cost: Cost
64
59
  is_resolved: boolean
@@ -297,21 +292,6 @@ function read_set(root: string, entry: string): ReadSet {
297
292
  return { entry, files, sections: sections_for(root, entry, files) }
298
293
  }
299
294
 
300
- function cost_of(text: string): Cost {
301
- return { bytes: Buffer.byteLength(text, 'utf8'), tokens: cost_tokens.estimate(text) }
302
- }
303
-
304
- function total(costs: ReadonlyArray<Cost>): Cost {
305
- const running = { bytes: NOTHING, tokens: NOTHING }
306
-
307
- for (const cost of costs) {
308
- running.bytes += cost.bytes
309
- running.tokens += cost.tokens
310
- }
311
-
312
- return running
313
- }
314
-
315
295
  function section_cost(root: string, reference: SectionReference): SectionCost {
316
296
  const found = document_section.section(read_document(root, reference.file), reference.heading)
317
297
 
@@ -334,74 +314,13 @@ function headings_for(sections: ReadonlyArray<SectionReference>, file: string):
334
314
  return sections.filter((reference) => reference.file === file).map((one) => one.heading)
335
315
  }
336
316
 
337
- function add_range(charged: Set<number>, found: Section): void {
338
- for (let index = found.first_line; index <= found.last_line; index += ONE_LINE) charged.add(index)
339
- }
340
-
341
- // `undefined` the moment one heading does not resolve, because that reference is charged at its whole
342
- // file and there is then nothing to union.
343
- function charged_lines(markdown: string, headings: ReadonlyArray<string>): Set<number> | undefined {
344
- const charged = new Set<number>()
345
-
346
- for (const heading of headings) {
347
- const found = document_section.section(markdown, heading)
348
-
349
- if (found === undefined) return undefined
350
-
351
- add_range(charged, found)
352
- }
353
-
354
- return charged
355
- }
356
-
357
- // **The union's charged lines are split into contiguous runs, and each run is costed on its own
358
- // natural text.** Joining non-adjacent lines with `\n` into one string fabricates a token boundary at
359
- // every gap the union skipped, so the estimate could drift a token *above* the per-reference sum even
360
- // with no real overlap — a false negative saving the measurement must never print
361
- // (joshuafolkken/kit#1934). A run is a maximal stretch of consecutive charged lines, which is exactly
362
- // the contiguous text a reader of that section actually reads, so summing the runs both mirrors the
363
- // real read and keeps `scoped` at or below the per-reference sum by construction.
364
- function extend_or_start(runs: Array<Array<number>>, index: number): void {
365
- const current = runs.at(-1)
366
-
367
- if (current?.at(-1) === index - ONE_LINE) current.push(index)
368
- else runs.push([index])
369
- }
370
-
371
- function contiguous_runs(charged: ReadonlySet<number>): Array<Array<number>> {
372
- const runs: Array<Array<number>> = []
373
- const ascending = [...charged].toSorted((left, right) => left - right)
374
-
375
- for (const index of ascending) extend_or_start(runs, index)
376
-
377
- return runs
378
- }
379
-
380
- function run_text(lines: ReadonlyArray<string>, run: ReadonlyArray<number>): string {
381
- return run.map((index) => lines[index] ?? '').join('\n')
382
- }
383
-
384
- // **Charged per file over the union of the lines its references cover, never per reference.**
385
- // Summing the references instead double-counts the two ways one file can be cited twice — two
386
- // unresolved headings each charged at the whole file, and a `##` section cited beside one of its own
387
- // `###` children, which `section()` already returns inside the parent. Either one could push the
388
- // scoped figure above the whole one and print a *negative* saving, which is the single direction this
389
- // measurement must not be able to move (joshuafolkken/kit#1776 review round 1).
390
- function file_scoped_cost(root: string, file: string, headings: ReadonlyArray<string>): Cost {
391
- const markdown = read_document(root, file)
392
- const charged = charged_lines(markdown, headings)
393
-
394
- if (charged === undefined) return cost_of(markdown)
395
-
396
- const lines = markdown.split('\n')
397
-
398
- return total(contiguous_runs(charged).map((run) => cost_of(run_text(lines, run))))
399
- }
400
-
317
+ // The per-file union that keeps `scoped` at or below `whole` lives in `read-set-cost.ts`.
401
318
  function scoped_cost(root: string, sections: ReadonlyArray<SectionReference>): Cost {
402
319
  const files = unique(sections.map((reference) => reference.file))
403
320
 
404
- return total(files.map((file) => file_scoped_cost(root, file, headings_for(sections, file))))
321
+ return total(
322
+ files.map((file) => scoped_file_cost(read_document(root, file), headings_for(sections, file))),
323
+ )
405
324
  }
406
325
 
407
326
  function file_costs(root: string, names: ReadonlyArray<string>): Array<FileCost> {
@@ -491,5 +410,7 @@ const entry_read_set = {
491
410
  total,
492
411
  }
493
412
 
494
- export type { Cost, FileCost, ReadSet, ReadSetCost, SectionCost, SectionReference }
413
+ export type { FileCost, ReadSet, ReadSetCost, SectionCost, SectionReference }
495
414
  export { entry_read_set }
415
+
416
+ export { type Cost } from './read-set-cost'
@@ -0,0 +1,98 @@
1
+ import { cost_tokens } from '#scripts/cost-runtime/cost-tokens'
2
+ import { document_section, type Section } from './document-section'
3
+
4
+ // What a read costs, and the scoped union that charges one file's cited sections once — split out
5
+ // of `entry-read-set.ts` when it reached its line limit (joshuafolkken/kit#3264). `entry_read_set`
6
+ // re-exports `cost_of`, `total` and `Cost` under the names they always had, so the move changed no
7
+ // call site.
8
+
9
+ const NOTHING = 0
10
+ const ONE_LINE = 1
11
+
12
+ interface Cost {
13
+ bytes: number
14
+ tokens: number
15
+ }
16
+
17
+ function cost_of(text: string): Cost {
18
+ return { bytes: Buffer.byteLength(text, 'utf8'), tokens: cost_tokens.estimate(text) }
19
+ }
20
+
21
+ function total(costs: ReadonlyArray<Cost>): Cost {
22
+ const running = { bytes: NOTHING, tokens: NOTHING }
23
+
24
+ for (const cost of costs) {
25
+ running.bytes += cost.bytes
26
+ running.tokens += cost.tokens
27
+ }
28
+
29
+ return running
30
+ }
31
+
32
+ function add_range(charged: Set<number>, found: Section): void {
33
+ for (let index = found.first_line; index <= found.last_line; index += ONE_LINE) charged.add(index)
34
+ }
35
+
36
+ // `undefined` the moment one heading does not resolve, because that reference is charged at its whole
37
+ // file and there is then nothing to union.
38
+ function charged_lines(markdown: string, headings: ReadonlyArray<string>): Set<number> | undefined {
39
+ const charged = new Set<number>()
40
+
41
+ for (const heading of headings) {
42
+ const found = document_section.section(markdown, heading)
43
+
44
+ if (found === undefined) return undefined
45
+
46
+ add_range(charged, found)
47
+ }
48
+
49
+ return charged
50
+ }
51
+
52
+ // **The union's charged lines are split into contiguous runs, and each run is costed on its own
53
+ // natural text.** Joining non-adjacent lines with `\n` into one string fabricates a token boundary at
54
+ // every gap the union skipped, so the estimate could drift a token *above* the per-reference sum even
55
+ // with no real overlap — a false negative saving the measurement must never print
56
+ // (joshuafolkken/kit#1934). A run is a maximal stretch of consecutive charged lines, which is exactly
57
+ // the contiguous text a reader of that section actually reads, so summing the runs both mirrors the
58
+ // real read and keeps `scoped` at or below the per-reference sum by construction.
59
+ function extend_or_start(runs: Array<Array<number>>, index: number): void {
60
+ const current = runs.at(-1)
61
+
62
+ if (current?.at(-1) === index - ONE_LINE) current.push(index)
63
+ else runs.push([index])
64
+ }
65
+
66
+ function contiguous_runs(charged: ReadonlySet<number>): Array<Array<number>> {
67
+ const runs: Array<Array<number>> = []
68
+ const ascending = [...charged].toSorted((left, right) => left - right)
69
+
70
+ for (const index of ascending) extend_or_start(runs, index)
71
+
72
+ return runs
73
+ }
74
+
75
+ function run_text(lines: ReadonlyArray<string>, run: ReadonlyArray<number>): string {
76
+ return run.map((index) => lines[index] ?? '').join('\n')
77
+ }
78
+
79
+ // **Charged per file over the union of the lines its references cover, never per reference.**
80
+ // Summing the references instead double-counts the two ways one file can be cited twice — two
81
+ // unresolved headings each charged at the whole file, and a `##` section cited beside one of its own
82
+ // `###` children, which `section()` already returns inside the parent. Either one could push the
83
+ // scoped figure above the whole one and print a *negative* saving, which is the single direction this
84
+ // measurement must not be able to move (joshuafolkken/kit#1776 review round 1).
85
+ function scoped_file_cost(markdown: string, headings: ReadonlyArray<string>): Cost {
86
+ const charged = charged_lines(markdown, headings)
87
+
88
+ if (charged === undefined) return cost_of(markdown)
89
+
90
+ const lines = markdown.split('\n')
91
+
92
+ return total(contiguous_runs(charged).map((run) => cost_of(run_text(lines, run))))
93
+ }
94
+
95
+ const read_set_cost = { cost_of, scoped_file_cost, total }
96
+
97
+ export type { Cost }
98
+ export { read_set_cost }
@@ -147,6 +147,7 @@ function splice_declaration(
147
147
  if (index === input.first) return [...input.rendered]
148
148
  if (input.dropped.has(index)) return []
149
149
 
150
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- one return type (sonarjs/function-return-type)
150
151
  return [line]
151
152
  })
152
153
  }
@@ -301,7 +301,7 @@ function reported_references(
301
301
  .flatMap((reference) => {
302
302
  const state = reported_state(states, reference)
303
303
 
304
- return state === undefined ? [] : [{ reference, state }]
304
+ return state === undefined ? [] : { reference, state }
305
305
  })
306
306
  }
307
307
 
@@ -73,9 +73,10 @@ function order_pairs(
73
73
  const blocked = find_child(children, { repo: epic_repo, number: link.blocked })
74
74
  if (blocker === undefined || blocked === undefined) return []
75
75
 
76
- return is_open_local(blocker, current_repo) && is_open_local(blocked, current_repo)
77
- ? [{ blocker, blocked }]
78
- : []
76
+ if (!is_open_local(blocker, current_repo) || !is_open_local(blocked, current_repo)) return []
77
+
78
+ // eslint-disable-next-line unicorn/no-unnecessary-array-flat-map -- one return type (sonarjs/function-return-type)
79
+ return [{ blocker, blocked }]
79
80
  })
80
81
  }
81
82
 
@@ -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 = {