@joshuafolkken/kit 1.1010.0 → 1.1050.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 (350) hide show
  1. package/.claude/settings.json +1 -5
  2. package/.claude/skills/dependency-update/SKILL.md +6 -3
  3. package/.claude/skills/epic-commands/SKILL.md +46 -17
  4. package/.claude/skills/verify-ui/SKILL.md +1 -1
  5. package/.claude/skills/workflow-commands/SKILL.md +10 -25
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
  11. package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
  12. package/.claude/skills/workflow-commands/backlogrun.md +3 -12
  13. package/.claude/skills/workflow-commands/chain-rule.md +2 -2
  14. package/.claude/skills/workflow-commands/followup-reference.md +2 -2
  15. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
  16. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  18. package/.claude/skills/workflow-commands/into-target.md +1 -1
  19. package/.claude/skills/workflow-commands/issue-comments.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-filing.md +80 -126
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
  22. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  23. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
  24. package/.codex/hooks.json +1 -5
  25. package/.github/workflows/auto-tag.yml +21 -0
  26. package/.github/workflows/publish.yml +9 -10
  27. package/.github/workflows/sonar-qube.yml +1 -1
  28. package/AGENTS.md +4 -25
  29. package/CLAUDE.md +24 -38
  30. package/GEMINI.md +4 -24
  31. package/README.md +22 -56
  32. package/dist/CLAUDE.md +24 -38
  33. package/dist/hooks/batch-guard.js +4 -4
  34. package/dist/hooks/{chunk-6PSTVOLH.js → chunk-5BK3K7V4.js} +114 -90
  35. package/dist/hooks/{chunk-PAXMLGSS.js → chunk-7VPDUBRN.js} +93 -175
  36. package/dist/hooks/{chunk-KPZN42RT.js → chunk-DMQN45FN.js} +3 -3
  37. package/dist/hooks/{chunk-WMIR5SNX.js → chunk-EJDX2TOB.js} +13 -3
  38. package/dist/hooks/{chunk-A2H5I2VD.js → chunk-GQ7KP45M.js} +2 -2
  39. package/dist/hooks/{chunk-4GDR2YTV.js → chunk-HJIISEAC.js} +4 -4
  40. package/dist/hooks/{chunk-AWTFKKFW.js → chunk-NAS4C7GP.js} +6 -33
  41. package/dist/hooks/{chunk-56LGMJJV.js → chunk-NJQOBE6L.js} +2 -2
  42. package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-RLDX5SOC.js} +1222 -941
  43. package/dist/hooks/{chunk-GBJUERYK.js → chunk-WQ4OSBSS.js} +10 -3
  44. package/dist/hooks/codex-hook-adapter.js +10 -10
  45. package/dist/hooks/duplicate-read-guard.js +5 -5
  46. package/dist/hooks/format-edited.js +15 -34
  47. package/dist/hooks/investigation-guard.js +5 -5
  48. package/dist/hooks/pretool-guard.js +23 -16
  49. package/dist/hooks/stop-guard.js +21 -200
  50. package/dist/josh.js +42 -125
  51. package/dist/version/index.js +466 -42
  52. package/eslint/base.js +2 -11
  53. package/package.json +36 -10
  54. package/prompts/coding-standards.md +15 -290
  55. package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
  56. package/prompts/collaboration-workflow/epic-audit.md +2 -53
  57. package/prompts/collaboration-workflow/epic-bundle.md +2 -106
  58. package/prompts/collaboration-workflow/file-edits.md +6 -53
  59. package/prompts/collaboration-workflow/issue-template.md +4 -4
  60. package/prompts/collaboration-workflow/operating-rules.md +18 -75
  61. package/prompts/collaboration-workflow/principles.md +74 -0
  62. package/prompts/collaboration-workflow/residency.md +55 -15
  63. package/prompts/collaboration-workflow/rule-delivery.md +5 -46
  64. package/prompts/collaboration-workflow/shell-body.md +7 -9
  65. package/prompts/collaboration-workflow/turn-batching.md +21 -37
  66. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  67. package/prompts/collaboration-workflow/wip-cap.md +15 -18
  68. package/prompts/collaboration-workflow.md +22 -28
  69. package/prompts/sonar-hotspot-handling.md +1 -1
  70. package/scripts/adopt/adopt-logic.ts +2 -2
  71. package/scripts/adopt/adopt-toolkits.ts +3 -1
  72. package/scripts/adopt/adopt.ts +2 -2
  73. package/scripts/agent/agent-event.ts +2 -9
  74. package/scripts/agent/agent-exit-record.ts +2 -9
  75. package/scripts/auto-ok/auto-ok-cli.ts +9 -9
  76. package/scripts/backlog/backlog-budget-cli.ts +2 -6
  77. package/scripts/backlog/backlog-drive-cli.ts +20 -10
  78. package/scripts/backlog/backlog-drive.ts +50 -16
  79. package/scripts/backlog/backlog-next.ts +24 -8
  80. package/scripts/backlog/backlog-offer-cli.ts +6 -5
  81. package/scripts/backlog/backlog-plan-cli.ts +5 -1
  82. package/scripts/backlog/backlog-plan.ts +1 -1
  83. package/scripts/backlog/backlog-pool.ts +31 -11
  84. package/scripts/backlog/backlog-rank.ts +106 -0
  85. package/scripts/backlog/backlog-waves.ts +42 -29
  86. package/scripts/build/build-bin.ts +2 -1
  87. package/scripts/build/build-config-merge.ts +2 -1
  88. package/scripts/build/build-hooks.ts +31 -6
  89. package/scripts/build/build-managed-marker.ts +2 -1
  90. package/scripts/build/build-self-sync-guard.ts +2 -1
  91. package/scripts/build/build-version.ts +2 -1
  92. package/scripts/cases/cases-logic.ts +2 -6
  93. package/scripts/ci/pr-classification.ts +13 -11
  94. package/scripts/cost-runtime/cost-transcript.ts +4 -11
  95. package/scripts/delegation/delegation-policy.ts +11 -0
  96. package/scripts/doctor/doctor-consumer.ts +5 -12
  97. package/scripts/doctor/doctor-ports.ts +2 -2
  98. package/scripts/document/document-byte-budget.ts +13 -21
  99. package/scripts/document/document-section.ts +2 -6
  100. package/scripts/document/entry-read-budget.ts +6 -5
  101. package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
  102. package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
  103. package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
  104. package/scripts/epic/epic-audit-cli.ts +4 -4
  105. package/scripts/epic/epic-audit-orphans.ts +2 -2
  106. package/scripts/epic/epic-audit-rationale.ts +1 -1
  107. package/scripts/epic/epic-audit.ts +1 -1
  108. package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
  109. package/scripts/epic/epic-bundle-cli.ts +1 -1
  110. package/scripts/epic/epic-bundle.ts +2 -2
  111. package/scripts/epic/epic-candidate-confirm.ts +2 -1
  112. package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
  113. package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
  114. package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
  115. package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
  116. package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
  117. package/scripts/epic/epic-cross-repo.ts +21 -38
  118. package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
  119. package/scripts/epic/epic-fetch.ts +3 -3
  120. package/scripts/epic/epic-graph.ts +3 -3
  121. package/scripts/epic/epic-index.ts +2 -2
  122. package/scripts/epic/epic-issue.ts +1 -1
  123. package/scripts/epic/epic-lane-offer.ts +11 -22
  124. package/scripts/epic/epic-next.ts +3 -3
  125. package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
  126. package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
  127. package/scripts/epic/epic-rank.ts +37 -0
  128. package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
  129. package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
  130. package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
  131. package/scripts/epic/epic-relation-recheck.ts +4 -3
  132. package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
  133. package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
  134. package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
  135. package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
  136. package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
  137. package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
  138. package/scripts/epic/epic-solo.ts +29 -28
  139. package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
  140. package/{scripts-ai → scripts/epic}/epic.ts +17 -16
  141. package/scripts/gh/fix-gh-packages.ts +2 -1
  142. package/scripts/gh/gh-spawn.ts +12 -10
  143. package/scripts/git/constants.ts +1 -1
  144. package/scripts/git/git-branch.ts +10 -1
  145. package/scripts/git/git-command.ts +2 -2
  146. package/scripts/git/git-error.ts +2 -5
  147. package/scripts/git/git-execa-error.ts +6 -1
  148. package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
  149. package/scripts/git/git-followup-flush.ts +74 -48
  150. package/scripts/git/git-followup-issue-close.ts +2 -2
  151. package/scripts/git/git-followup-label.ts +1 -1
  152. package/scripts/git/git-followup-pending.ts +1 -1
  153. package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
  154. package/scripts/git/git-gh-exec.ts +85 -26
  155. package/scripts/git/git-gh-issue-read.ts +2 -2
  156. package/scripts/git/git-gh-issue-rest.ts +2 -2
  157. package/scripts/git/git-gh-issue-write.ts +6 -6
  158. package/scripts/git/git-gh-pr-read.ts +0 -21
  159. package/scripts/git/git-gh-pr.ts +2 -23
  160. package/scripts/git/git-next-issues.ts +34 -5
  161. package/scripts/git/git-pr-followup-wrapup.ts +2 -6
  162. package/scripts/git/git-preflight.ts +25 -10
  163. package/scripts/git/git-push-transport.ts +12 -69
  164. package/scripts/git/git-spawn.ts +79 -3
  165. package/scripts/git/git-ssh-keepalive.ts +74 -0
  166. package/scripts/git/git-staging.ts +21 -9
  167. package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
  168. package/scripts/git/github-issue-url.ts +1 -1
  169. package/scripts/git/gone-branch.ts +3 -2
  170. package/scripts/git/issue-labels.ts +13 -1
  171. package/scripts/git/issue-rank.ts +89 -0
  172. package/scripts/git/issue-reference.ts +19 -0
  173. package/scripts/git/main-merge.ts +2 -1
  174. package/scripts/git/main-sync.ts +3 -2
  175. package/scripts/git/schemas.ts +1 -1
  176. package/scripts/git/stash-pop-args.ts +7 -13
  177. package/scripts/git/stash-pop-cli.ts +2 -1
  178. package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
  179. package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
  180. package/scripts/hooks/format-edited-cli.ts +15 -0
  181. package/scripts/hooks/format-edited-file.ts +2 -9
  182. package/scripts/hooks/format-rewrite-notice.ts +2 -8
  183. package/scripts/hooks/pretool-guard-cli.ts +21 -0
  184. package/scripts/hooks/pretool-guard.ts +14 -15
  185. package/scripts/init/init-ai-copy.ts +2 -5
  186. package/scripts/init/init.ts +8 -4
  187. package/scripts/init/start-exec.ts +7 -1
  188. package/scripts/init/start-plan.ts +18 -5
  189. package/scripts/init/start-setup-pr.ts +46 -19
  190. package/scripts/init/start-steps.ts +43 -10
  191. package/scripts/init/start.ts +42 -11
  192. package/scripts/issue/issue-cite.ts +19 -1
  193. package/scripts/issue/issue-comment-cli.ts +2 -1
  194. package/scripts/issue/issue-file-cli.ts +2 -1
  195. package/scripts/issue/issue-fold-cli.ts +9 -16
  196. package/scripts/issue/issue-scout-cli.ts +8 -13
  197. package/scripts/issue/measure-rerun-cli.ts +7 -6
  198. package/scripts/josh/josh-command-map.ts +0 -102
  199. package/scripts/josh/josh-commands-ai.ts +2 -2
  200. package/scripts/josh/josh-commands-backlog.ts +1 -1
  201. package/scripts/josh/josh-commands-development.ts +3 -3
  202. package/scripts/josh/josh-commands-project.ts +1 -1
  203. package/scripts/josh/josh-commands-workflow.ts +4 -4
  204. package/scripts/lane/lane-child-marker.ts +16 -2
  205. package/scripts/lane/lane-cli.ts +34 -15
  206. package/scripts/lane/lane-open.ts +3 -2
  207. package/scripts/lane/lane-registry.ts +3 -6
  208. package/scripts/lane/lane-report.ts +10 -4
  209. package/scripts/lib/cli-flags.ts +20 -2
  210. package/scripts/lib/error-message.ts +9 -0
  211. package/scripts/lib/read-file.ts +26 -2
  212. package/scripts/observations/observation-ledger-home.ts +98 -26
  213. package/scripts/observations/observation-ledger-migrate.ts +26 -21
  214. package/scripts/observations/observation-ledger-prepare.ts +10 -20
  215. package/scripts/observations/observation-ledger.ts +58 -25
  216. package/scripts/observations/observations-flush-cli.ts +6 -23
  217. package/scripts/observations/observations-flush-landing.ts +2 -1
  218. package/scripts/observations/observations-flush.ts +29 -20
  219. package/scripts/package/package-scout-cli.ts +8 -16
  220. package/scripts/ports/port-command.ts +2 -1
  221. package/scripts/propagate/propagate-plan.ts +185 -0
  222. package/scripts/propagate/propagate-publish.ts +6 -7
  223. package/scripts/propagate/propagate-run.ts +5 -1
  224. package/scripts/propagate/propagate-steps.ts +18 -2
  225. package/scripts/propagate/propagate-targets.ts +13 -2
  226. package/scripts/propagate/propagate.ts +63 -44
  227. package/scripts/refactor/import-graph.ts +2 -6
  228. package/scripts/registry-migration/migrate-logic.ts +1 -0
  229. package/scripts/registry-migration/migrate.ts +3 -2
  230. package/scripts/registry-migration/user-npmrc.ts +2 -2
  231. package/scripts/release/github-release.ts +1 -1
  232. package/scripts/release/release-cli.ts +2 -1
  233. package/scripts/repo/repo-setting.ts +10 -8
  234. package/scripts/review/review-attest.ts +44 -0
  235. package/scripts/review/review-brief-cli.ts +49 -2
  236. package/scripts/review/review-brief.ts +46 -1
  237. package/scripts/review/review-finding-ledger.ts +3 -3
  238. package/scripts/review/review-findings-cli.ts +4 -11
  239. package/scripts/review/review-record-cli.ts +14 -11
  240. package/scripts/review/review-record.ts +6 -16
  241. package/scripts/review/review-stamps.ts +7 -0
  242. package/scripts/rules/decision-oracle.ts +2 -2
  243. package/scripts/rules/implementation-cut.ts +1 -1
  244. package/scripts/rules/issue-fold.ts +1 -2
  245. package/scripts/rules/lane-carry-conflict.ts +1 -2
  246. package/scripts/rules/lane-park.ts +1 -2
  247. package/scripts/rules/pre-gate-cut.ts +2 -2
  248. package/scripts/rules/rule-body-guard.ts +2 -2
  249. package/scripts/rules/rule-value-cache.ts +99 -0
  250. package/scripts/rules/rule-value-cli.ts +10 -9
  251. package/scripts/rules/rule-value.ts +1 -1
  252. package/scripts/run/run-carry-args.ts +2 -6
  253. package/scripts/run/run-carry-cli.ts +5 -7
  254. package/scripts/run/run-cut-args.ts +7 -13
  255. package/scripts/run/run-ending-cli.ts +4 -7
  256. package/scripts/run/run-event-stream.ts +2 -5
  257. package/scripts/run/run-headless.ts +22 -6
  258. package/scripts/run/run-liveness-cli.ts +4 -7
  259. package/scripts/run/run-parent-cut-hook.ts +144 -0
  260. package/scripts/run/run-progress-args.ts +2 -6
  261. package/scripts/run/run-progress-cli.ts +2 -1
  262. package/scripts/run/run-progress-read.ts +2 -1
  263. package/scripts/run/run-progress.ts +21 -8
  264. package/scripts/run/run-ship-cli.ts +6 -74
  265. package/scripts/run/run-ship-preflight.ts +87 -0
  266. package/scripts/run/run-ship-review-steps.ts +5 -26
  267. package/scripts/run/run-ship-review.ts +9 -0
  268. package/scripts/run/run-ship-scoped.ts +44 -0
  269. package/scripts/run/run-ship-stage.ts +5 -1
  270. package/scripts/run/run-ship-steps.ts +105 -0
  271. package/scripts/run/run-ship.ts +2 -0
  272. package/scripts/run/run-tail-cli.ts +3 -3
  273. package/scripts/run/run-tidy-stashes.ts +7 -14
  274. package/scripts/run/run-wake-cli.ts +4 -8
  275. package/scripts/run/run-wake-describe.ts +19 -1
  276. package/scripts/run/run-wake-driver.ts +15 -2
  277. package/scripts/run/run-wake-session.ts +14 -4
  278. package/scripts/run/run-watcher-hook.ts +1 -1
  279. package/scripts/safe-chain/project-config.ts +3 -6
  280. package/scripts/security/security-audit-provision.ts +6 -7
  281. package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
  282. package/scripts/sync/directory-copy-guard.ts +2 -1
  283. package/scripts/sync/sync-ai-files.ts +3 -3
  284. package/scripts/sync/template-source-logic.ts +3 -4
  285. package/scripts/test/e2e-retry-check.ts +3 -6
  286. package/scripts/time-runtime/time-bundle-call.ts +2 -2
  287. package/scripts/ui/ui-routes-cli.ts +3 -6
  288. package/scripts/version/bump-version.ts +2 -1
  289. package/scripts/version/npm-registry.ts +97 -0
  290. package/scripts/version/version-commands.ts +5 -4
  291. package/scripts/version/version-remote.ts +38 -33
  292. package/scripts/version/version-targets.ts +3 -3
  293. package/.claude/skills/workflow-commands/rule-residency.md +0 -267
  294. package/evals/scenarios/consult-not-execute.json +0 -20
  295. package/evals/scenarios/no-implicit-workflow.json +0 -26
  296. package/evals/scenarios/overrides-both-locations.json +0 -29
  297. package/evals/scenarios/upstream-interrupt.json +0 -23
  298. package/evals/scenarios/workflow-skill-loaded.json +0 -16
  299. package/prompts/agent-lint-protocol.json +0 -79
  300. package/prompts/agent-rules.md +0 -63
  301. package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
  302. package/prompts/collaboration-workflow/distributed-docs.md +0 -7
  303. package/prompts/collaboration-workflow/durable-rules.md +0 -24
  304. package/prompts/collaboration-workflow/latest-first.md +0 -14
  305. package/prompts/collaboration-workflow/no-clones.md +0 -11
  306. package/prompts/collaboration-workflow/simplicity-first.md +0 -9
  307. package/prompts/collaboration-workflow/single-source-rules.md +0 -13
  308. package/scripts/build/shim-shell.ts +0 -25
  309. package/scripts/claude/skill-meta.ts +0 -37
  310. package/scripts/document/document-scan.ts +0 -108
  311. package/scripts/document/generate-catalog.ts +0 -69
  312. package/scripts/document/retired-phrases.ts +0 -143
  313. package/scripts/document/review-agent-type.ts +0 -46
  314. package/scripts/document/single-source.ts +0 -73
  315. package/scripts/git/git-fixture-workspace.ts +0 -88
  316. package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
  317. package/scripts/hooks/prompt-hooks.ts +0 -38
  318. package/scripts/init/hook-command-bootstrap.ts +0 -83
  319. package/scripts/issue/issue-logic.ts +0 -50
  320. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
  321. package/scripts/josh/josh-script-reader.ts +0 -11
  322. package/scripts/package/production-dependency-graph.ts +0 -68
  323. package/scripts/retrospective/retrospective-cli.ts +0 -97
  324. package/scripts/retrospective/retrospective.ts +0 -127
  325. package/scripts/rules/delivered-rules-harness.ts +0 -72
  326. package/scripts/rules/top-level-scripts-guard.ts +0 -47
  327. package/scripts/run/run-carry-flush.ts +0 -58
  328. package/scripts/run/run-transcript.golden.txt +0 -57
  329. package/scripts/run/run-wake-work.ts +0 -108
  330. package/scripts/sync/dependabot-pin-logic.ts +0 -135
  331. package/scripts/test/classify-isolation.ts +0 -149
  332. package/scripts/test/http-fault-injection.ts +0 -79
  333. package/scripts/test/josh-harness-environment.ts +0 -260
  334. package/scripts/test/josh-harness.ts +0 -96
  335. package/scripts/test/pilot-files.ts +0 -452
  336. package/scripts/test/pilot-measure.ts +0 -78
  337. package/scripts/test/test-network-guard.ts +0 -453
  338. package/scripts/test/test-repository-guard.ts +0 -317
  339. package/scripts/test/test-state-guard.ts +0 -85
  340. package/scripts/test/test-stdout-guard.ts +0 -54
  341. package/scripts/test/test-telegram-guard.ts +0 -90
  342. package/scripts/test/unit-guard-environment.ts +0 -12
  343. package/scripts/test/unit-projects.ts +0 -139
  344. package/scripts/test/vitest-include-globs.ts +0 -12
  345. package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
  346. package/scripts-ai/completion-gate.ts +0 -13
  347. package/scripts-ai/environment-loader.ts +0 -9
  348. /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
  349. /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
  350. /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
@@ -1,29 +1,19 @@
1
- import { readFile } from 'node:fs/promises'
2
- import path from 'node:path'
3
- import { OBSERVATION_LEDGER_PATH } from './observation-ledger'
1
+ import { observation_ledger_home } from './observation-ledger-home'
4
2
  import { observation_ledger_line, type BrokenLine } from './observation-ledger-line'
5
- import { observation_ledger_migrate } from './observation-ledger-migrate'
6
3
 
7
- // **What has to hold before a ledger line is committed, asked by both of the paths that commit one**
8
- // (joshuafolkken/kit#2763): `pnpm josh observations:flush` and the run's own commit in
9
- // `scripts/git/git-staging.ts`. Lines still on the old path are moved first (joshuafolkken/kit#2724),
10
- // so the commit carries the move and the lines together, and every entry line is then parsed against
11
- // the grammar (joshuafolkken/kit#2123) — each caller decides what a broken line costs it.
4
+ // **What has to hold before a ledger line is committed, asked by every path that commits one**
5
+ // (joshuafolkken/kit#2763): `pnpm josh observations:flush`, the run's own commit in
6
+ // `scripts/git/git-staging.ts` and `pnpm josh followup`'s pre-merge ledger commit. Lines still on an
7
+ // old path are moved first (joshuafolkken/kit#2724, joshuafolkken/kit#2919), so the commit carries the
8
+ // move and the lines together, and every entry line is then parsed against the grammar
9
+ // (joshuafolkken/kit#2123) — each caller decides what a broken line costs it.
12
10
  //
13
11
  // **A ledger that is not there has no line to break** — the change a status saw is then its deletion,
14
12
  // and a commit carries that like any other change to it.
15
- async function read_ledger(root: string): Promise<string> {
16
- try {
17
- return await readFile(path.join(root, OBSERVATION_LEDGER_PATH), 'utf8')
18
- } catch {
19
- return ''
20
- }
21
- }
22
-
23
13
  async function prepare(root: string): Promise<ReadonlyArray<BrokenLine>> {
24
- observation_ledger_migrate.migrate(root)
25
-
26
- return observation_ledger_line.broken_ledger_lines(await read_ledger(root))
14
+ return observation_ledger_line.broken_ledger_lines(
15
+ (await observation_ledger_home.read(root)) ?? '',
16
+ )
27
17
  }
28
18
 
29
19
  const observation_ledger_prepare = { prepare }
@@ -1,22 +1,33 @@
1
- // The observation ledger's one path, named here rather than inside any of the places that act on it
2
- // (joshuafolkken/kit#1756). `git-staging.ts` stages it with the run's own commit when its grammar
1
+ // The observation ledger's one location, named here rather than inside any of the places that act on
2
+ // it (joshuafolkken/kit#1756). `git-staging.ts` stages it with the run's own commit when its grammar
3
3
  // holds (joshuafolkken/kit#2763), `hook-gate-reuse.ts` excludes it from what a hook calls a carried
4
4
  // tree, and `observations-flush.ts` commits what no run carried — so a literal in each would be three
5
5
  // answers to one question, and a rename reaching only some of them would silently split them.
6
6
  //
7
7
  // **It sits under `docs/maintainers/`, apart from the reader-facing documents** (joshuafolkken/kit#2724).
8
- const OBSERVATION_LEDGER_PATH = 'docs/maintainers/observations.md'
9
-
10
- // Where the ledger lived before joshuafolkken/kit#2724, kept as a migration source rather than
11
- // forgotten: a run still on the old code, a stash cut before the move and a consumer repository's
12
- // existing ledger all write here, and `observation-ledger-migrate.ts` moves what it finds to the path
13
- // above. Every place that recognizes the ledger recognizes both, so a line on either path is never
14
- // staged into an ordinary commit. Remove it once every repository holding a ledger has migrated.
15
- const LEGACY_OBSERVATION_LEDGER_PATH = 'docs/observations.md'
16
-
17
- const OBSERVATION_LEDGER_PATHS: ReadonlyArray<string> = [
18
- OBSERVATION_LEDGER_PATH,
19
- LEGACY_OBSERVATION_LEDGER_PATH,
8
+ //
9
+ // **It is a directory of one file per issue, not one file** (joshuafolkken/kit#2919). One file every
10
+ // run appended to made two lanes' pull requests conflict at its tail, so lanes parked their lines in
11
+ // the primary checkout for a batch-end flush — and another run's stash took them from there, and the
12
+ // flush never saw them. A run now appends to `<issue>.md` in its own work tree and merges it with its
13
+ // own pull request: parallel lanes write different files, so nothing conflicts and nothing waits.
14
+ const OBSERVATION_LEDGER_DIRECTORY = 'docs/maintainers/observations'
15
+
16
+ const LEDGER_FILE_EXTENSION = '.md'
17
+
18
+ // What the single-file ledgers held is moved here (`observation-ledger-migrate.ts`), so no line is
19
+ // lost to the split.
20
+ const LEGACY_LEDGER_FILE = `legacy${LEDGER_FILE_EXTENSION}`
21
+
22
+ // Where the ledger lived as one file — `docs/observations.md` before joshuafolkken/kit#2724 and
23
+ // `docs/maintainers/observations.md` until joshuafolkken/kit#2919 — kept as migration sources rather
24
+ // than forgotten: a run still on the old code, a stash cut before the move and a consumer repository's
25
+ // existing ledger all write there, and `observation-ledger-migrate.ts` moves what it finds into the
26
+ // directory. Every place that recognizes the ledger recognizes them, so a line on either path is never
27
+ // staged into an ordinary commit. Remove them once every repository holding a ledger has migrated.
28
+ const LEGACY_OBSERVATION_LEDGER_PATHS: ReadonlyArray<string> = [
29
+ 'docs/maintainers/observations.md',
30
+ 'docs/observations.md',
20
31
  ]
21
32
 
22
33
  // `git status --porcelain` prints `XY PATH`, so the path starts at the fourth character.
@@ -26,29 +37,48 @@ function status_path(line: string): string {
26
37
  return line.slice(STATUS_PATH_INDEX).trim()
27
38
  }
28
39
 
29
- // The suffix of the file a migration renames the old ledger to while it copies it
40
+ // The ledger file one writer appends to — an issue number, or a date for a line written outside any
41
+ // issue's run (`observation-ledger-home.ts` decides which).
42
+ function ledger_file(name: number | string): string {
43
+ return `${OBSERVATION_LEDGER_DIRECTORY}/${String(name)}${LEDGER_FILE_EXTENSION}`
44
+ }
45
+
46
+ function is_ledger_file_name(name: string): boolean {
47
+ return name.endsWith(LEDGER_FILE_EXTENSION)
48
+ }
49
+
50
+ // The suffix of the file a migration renames an old ledger to while it copies it
30
51
  // (`observation-ledger-migrate.ts`): `docs/observations.md.<pid>.migrating`.
31
52
  const MIGRATION_CLAIM_SUFFIX = '.migrating'
32
53
 
33
54
  // A claim left by a process killed mid-migration holds ledger lines, so it is the ledger too: never
34
55
  // staged by an ordinary commit, and absorbed by the next migration.
35
56
  function is_migration_claim(file_path: string): boolean {
36
- return (
37
- file_path.startsWith(`${LEGACY_OBSERVATION_LEDGER_PATH}.`) &&
38
- file_path.endsWith(MIGRATION_CLAIM_SUFFIX)
57
+ return LEGACY_OBSERVATION_LEDGER_PATHS.some(
58
+ (legacy) => file_path.startsWith(`${legacy}.`) && file_path.endsWith(MIGRATION_CLAIM_SUFFIX),
39
59
  )
40
60
  }
41
61
 
62
+ // Porcelain status collapses an untracked directory to `docs/maintainers/observations/`, so the
63
+ // directory itself, trailing slash and all, is the ledger as much as any file inside it.
64
+ function is_in_directory(file_path: string): boolean {
65
+ return file_path.startsWith(`${OBSERVATION_LEDGER_DIRECTORY}/`)
66
+ }
67
+
42
68
  function is_ledger_path(file_path: string): boolean {
43
- return OBSERVATION_LEDGER_PATHS.includes(file_path) || is_migration_claim(file_path)
69
+ return (
70
+ is_in_directory(file_path) ||
71
+ LEGACY_OBSERVATION_LEDGER_PATHS.includes(file_path) ||
72
+ is_migration_claim(file_path)
73
+ )
44
74
  }
45
75
 
46
76
  function is_ledger_line(line: string): boolean {
47
77
  return is_ledger_path(status_path(line))
48
78
  }
49
79
 
50
- // The ledger paths a porcelain status names — what a flush stages after a migration, which may be the
51
- // new file together with the old one's deletion.
80
+ // The ledger paths a porcelain status names — what a commit of the ledger stages, which may be a new
81
+ // file together with an old one's deletion.
52
82
  function ledger_paths(status_output: string): ReadonlyArray<string> {
53
83
  return status_output
54
84
  .split('\n')
@@ -57,7 +87,7 @@ function ledger_paths(status_output: string): ReadonlyArray<string> {
57
87
  }
58
88
 
59
89
  // **The one place that answers "does this working tree hold a pending observation append?"**
60
- // `observations-flush.ts`'s `has_ledger_change` and `pnpm josh followup`'s pre-flush short-circuit
90
+ // `observations-flush.ts`'s `has_ledger_change` and `pnpm josh followup`'s pre-merge ledger commit
61
91
  // both ask it (joshuafolkken/kit#1810), so a literal in each would be the clone the header above
62
92
  // warns against. A blank porcelain line carries no ledger path, so `is_ledger_line` answers false for
63
93
  // it and no filtering is needed.
@@ -67,17 +97,20 @@ function has_pending_append(status_output: string): boolean {
67
97
 
68
98
  const observation_ledger = {
69
99
  has_pending_append,
100
+ is_ledger_file_name,
70
101
  is_ledger_line,
71
102
  is_ledger_path,
72
103
  is_migration_claim,
104
+ ledger_file,
73
105
  ledger_paths,
74
106
  status_path,
75
107
  }
76
108
 
77
109
  export {
78
- LEGACY_OBSERVATION_LEDGER_PATH,
110
+ LEDGER_FILE_EXTENSION,
111
+ LEGACY_LEDGER_FILE,
112
+ LEGACY_OBSERVATION_LEDGER_PATHS,
79
113
  MIGRATION_CLAIM_SUFFIX,
80
- OBSERVATION_LEDGER_PATH,
81
- OBSERVATION_LEDGER_PATHS,
114
+ OBSERVATION_LEDGER_DIRECTORY,
82
115
  observation_ledger,
83
116
  }
@@ -1,38 +1,21 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { observation_ledger_home } from './observation-ledger-home'
3
+ import { error_text } from '#scripts/lib/error-message'
4
4
  import { observations_flush } from './observations-flush'
5
5
 
6
6
  const FAILURE_EXIT_CODE = 1
7
7
 
8
- function message_of(error: unknown): string {
9
- return error instanceof Error ? error.message : String(error)
10
- }
11
-
12
- // **From a lane, the flush acts on the primary checkout** (joshuafolkken/kit#2419). That is where the
13
- // ledger lives (`observation-ledger-home.ts`), and a lane's own checkout is never the one a flush may
14
- // switch — so the process moves there first, and every refusal after it is about that checkout, which
15
- // the prefix names so a "run `pnpm josh ms` first" is read as advice for the primary checkout.
16
- function enter_ledger_home(): string {
17
- if (!observation_ledger_home.is_lane()) return ''
18
-
19
- const root = observation_ledger_home.ledger_root()
20
-
21
- process.chdir(root)
22
-
23
- return `The observation ledger lives in the primary checkout, so this flush acts on ${root}: `
24
- }
25
-
26
8
  // The refusals are the command's whole safety story, so they are printed as the message rather than
27
9
  // as a stack: "this checkout is on a feature branch" and "the tree holds somebody else's work" are
28
10
  // both things the person fixes in one step, and a trace hides the sentence that says which.
11
+ //
12
+ // **It acts on the checkout it runs in** (joshuafolkken/kit#2919). A lane's ledger lines merge with
13
+ // the lane's own pull request, so a lane has nothing to flush and is refused like any feature branch.
29
14
  async function main(): Promise<void> {
30
- const prefix = enter_ledger_home()
31
-
32
15
  try {
33
- console.info(`${prefix}${await observations_flush.flush(new Date())}`)
16
+ console.info(await observations_flush.flush(new Date()))
34
17
  } catch (error) {
35
- console.error(`${prefix}${message_of(error)}`)
18
+ console.error(error_text.message_of(error))
36
19
  process.exit(FAILURE_EXIT_CODE)
37
20
  }
38
21
  }
@@ -1,6 +1,7 @@
1
1
  import { git_gh_command } from '#scripts/git/git-gh-command'
2
2
  import type { OpenPull } from '#scripts/git/git-gh-pr-auto-merge'
3
3
  import { git_pr_checks } from '#scripts/git/git-pr-checks'
4
+ import { error_text } from '#scripts/lib/error-message'
4
5
 
5
6
  // **A flush pull request lands whether or not the flush that opened it is still alive**
6
7
  // (joshuafolkken/kit#2497). The merge used to happen only inside the flush's own wait, so a session
@@ -25,7 +26,7 @@ async function request_auto_merge(branch_name: string): Promise<boolean> {
25
26
 
26
27
  return true
27
28
  } catch (error) {
28
- console.warn(AUTO_MERGE_WARNING, error instanceof Error ? error.message : String(error))
29
+ console.warn(AUTO_MERGE_WARNING, error_text.message_of(error))
29
30
 
30
31
  return false
31
32
  }
@@ -1,15 +1,17 @@
1
1
  import { git_command } from '#scripts/git/git-command'
2
2
  import { git_gh_command } from '#scripts/git/git-gh-command'
3
3
  import { main_sync } from '#scripts/git/main-sync'
4
- import { observation_ledger, OBSERVATION_LEDGER_PATH } from './observation-ledger'
4
+ import { error_text } from '#scripts/lib/error-message'
5
+ import { observation_ledger, OBSERVATION_LEDGER_DIRECTORY } from './observation-ledger'
5
6
  import type { BrokenLine } from './observation-ledger-line'
6
7
  import { observation_ledger_prepare } from './observation-ledger-prepare'
7
8
  import { observations_flush_landing } from './observations-flush-landing'
8
9
 
9
- // The commit path for the ledger lines no run's own commit carried (joshuafolkken/kit#1756). A run in
10
- // the primary checkout commits its appended lines with its own pull request (`scripts/git/
11
- // git-staging.ts`, joshuafolkken/kit#2763), so what is left here is a lane's lines — they sit in the
12
- // primary checkout, which a lane's commit cannot see — and whatever was appended after a commit.
10
+ // The commit path for the ledger lines no run's own commit carried (joshuafolkken/kit#1756). Every run —
11
+ // a lane's included — commits its appended lines with its own pull request (`scripts/git/
12
+ // git-staging.ts`, joshuafolkken/kit#2763, joshuafolkken/kit#2919), and `pnpm josh followup` commits a
13
+ // line appended after that commit before it merges, so what is left here is a line written on the
14
+ // default branch outside any issue's run — the date-named file `observation-ledger-home.ts` resolves.
13
15
  //
14
16
  // **The shape is `scripts/release/release-publish.ts`'s, deliberately.** Both open a pull request of
15
17
  // their own over one path, wait on the same required checks every other pull request waits on, and
@@ -19,7 +21,7 @@ import { observations_flush_landing } from './observations-flush-landing'
19
21
  // below does not survive — `observations-flush-landing.ts` carries why.
20
22
 
21
23
  const COMMIT_MESSAGE = 'Record observation ledger entries'
22
- const CLEAN_MESSAGE = `clean — ${OBSERVATION_LEDGER_PATH} matches the commit it sits on, so there is nothing to flush`
24
+ const CLEAN_MESSAGE = `clean — ${OBSERVATION_LEDGER_DIRECTORY} matches the commit it sits on, so there is nothing to flush`
23
25
  const BRANCH_PREFIX = 'observations/'
24
26
  const SUCCESS_EXIT_CODE = 0
25
27
  const RETURN_FAILURE_MESSAGE =
@@ -55,7 +57,7 @@ function branch_name_for(stamp: string): string {
55
57
  // single issue, and the ledger's own lines say where each one was seen.
56
58
  function pull_request_body(): string {
57
59
  return [
58
- `Appended observations from \`${OBSERVATION_LEDGER_PATH}\` that no run's own commit carried.`,
60
+ `Appended observations from \`${OBSERVATION_LEDGER_DIRECTORY}\` that no run's own commit carried.`,
59
61
  '',
60
62
  'The ledger is append-only, and a second line under one key is what promotes an observation to an issue — so this pull request adds lines and changes none.',
61
63
  '',
@@ -82,10 +84,6 @@ function other_changed_paths(status_output: string): ReadonlyArray<string> {
82
84
  )
83
85
  }
84
86
 
85
- function message_of(error: unknown): string {
86
- return error instanceof Error ? error.message : String(error)
87
- }
88
-
89
87
  // **The branch a failed flush leaves behind holds the only copy of those lines**, because the working
90
88
  // tree no longer shows them once they are committed. Sending a person to `pnpm josh ms` here — which
91
89
  // is what the ordinary off-default refusal says — would abandon them without a word, so this branch
@@ -205,7 +203,9 @@ async function catch_up_default(default_branch: string): Promise<void> {
205
203
  try {
206
204
  await git_command.merge_fast_forward(default_branch)
207
205
  } catch (error) {
208
- throw new Error(behind_default_message(default_branch, message_of(error)), { cause: error })
206
+ throw new Error(behind_default_message(default_branch, error_text.message_of(error)), {
207
+ cause: error,
208
+ })
209
209
  }
210
210
  }
211
211
 
@@ -242,7 +242,9 @@ async function return_to_default_branch(default_branch: string): Promise<void> {
242
242
  try {
243
243
  await git_command.fast_forward_local(default_branch)
244
244
  } catch (error) {
245
- throw new Error(`${FAST_FORWARD_FAILURE_MESSAGE} ${message_of(error)}`, { cause: error })
245
+ throw new Error(`${FAST_FORWARD_FAILURE_MESSAGE} ${error_text.message_of(error)}`, {
246
+ cause: error,
247
+ })
246
248
  }
247
249
 
248
250
  const exit_code = await main_sync.run([])
@@ -258,7 +260,9 @@ async function push_and_open(branch_name: string): Promise<string> {
258
260
 
259
261
  return await git_gh_command.pr_create(COMMIT_MESSAGE, pull_request_body(), 'ignore-for-release')
260
262
  } catch (error) {
261
- throw new Error(stranded_branch_message(branch_name, message_of(error)), { cause: error })
263
+ throw new Error(stranded_branch_message(branch_name, error_text.message_of(error)), {
264
+ cause: error,
265
+ })
262
266
  }
263
267
  }
264
268
 
@@ -279,7 +283,7 @@ async function roll_back(branch_name: string, default_branch: string): Promise<s
279
283
 
280
284
  return undefined
281
285
  } catch (error) {
282
- return message_of(error)
286
+ return error_text.message_of(error)
283
287
  }
284
288
  }
285
289
 
@@ -315,9 +319,12 @@ async function commit_ledger(branch_name: string, default_branch: string): Promi
315
319
  } catch (error) {
316
320
  const roll_back_reason = await roll_back(branch_name, default_branch)
317
321
 
318
- throw new Error(rejection_message(branch_name, message_of(error), roll_back_reason), {
319
- cause: error,
320
- })
322
+ throw new Error(
323
+ rejection_message(branch_name, error_text.message_of(error), roll_back_reason),
324
+ {
325
+ cause: error,
326
+ },
327
+ )
321
328
  }
322
329
  }
323
330
 
@@ -345,7 +352,9 @@ async function land(
345
352
  try {
346
353
  await observations_flush_landing.wait_for_landing(branch_name, is_auto_merge)
347
354
  } catch (error) {
348
- throw new Error(stranded_branch_message(branch_name, message_of(error)), { cause: error })
355
+ throw new Error(stranded_branch_message(branch_name, error_text.message_of(error)), {
356
+ cause: error,
357
+ })
349
358
  }
350
359
 
351
360
  await return_to_default_branch(default_branch)
@@ -366,7 +375,7 @@ function merged_message(branch_name: string): string {
366
375
  function broken_lines_message(broken: ReadonlyArray<BrokenLine>): string {
367
376
  const rows = broken.map((entry) => ` ${entry.line}\n ↳ ${entry.reason}`).join('\n')
368
377
 
369
- return `\`${OBSERVATION_LEDGER_PATH}\` has ${String(broken.length)} line(s) that break the ledger grammar, so \`pnpm josh observations:flush\` stops rather than committing them:\n${rows}`
378
+ return `\`${OBSERVATION_LEDGER_DIRECTORY}\` has ${String(broken.length)} line(s) that break the ledger grammar, so the ledger commit stops rather than carrying them:\n${rows}`
370
379
  }
371
380
 
372
381
  // **A malformed line is refused before a branch is cut, not after.** The ledger path was collected
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { parseArgs } from 'node:util'
3
+ import { cli_flags } from '#scripts/lib/cli-flags'
4
4
  import { z } from 'zod'
5
5
  import {
6
6
  package_scout,
@@ -120,27 +120,19 @@ function parse_size(raw: string | undefined): number {
120
120
  return raw === undefined || !Number.isSafeInteger(parsed) || parsed <= 0 ? DEFAULT_SIZE : parsed
121
121
  }
122
122
 
123
- function parse_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
124
- const { values, positionals } = parseArgs({
123
+ // An unknown flag is `undefined` rather than a throw, so the answer is the usage line rather than a
124
+ // stack trace, on a command whose output a workflow reads.
125
+ function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
126
+ const parsed = cli_flags.parse_or_undefined({
125
127
  args: [...argv],
126
128
  options: { size: { type: 'string' } },
127
129
  allowPositionals: true,
128
130
  })
129
- const keywords = positionals.join(' ').trim()
130
-
131
- if (keywords === '') return undefined
131
+ const keywords = parsed?.positionals.join(' ').trim() ?? ''
132
132
 
133
- return { keywords, size: parse_size(values.size) }
134
- }
133
+ if (parsed === undefined || keywords === '') return undefined
135
134
 
136
- // An unknown flag makes `parseArgs` throw. Caught so the answer is the usage line rather than a stack
137
- // trace, on a command whose output a workflow reads.
138
- function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
139
- try {
140
- return parse_arguments(argv)
141
- } catch {
142
- return undefined
143
- }
135
+ return { keywords, size: parse_size(parsed.values.size) }
144
136
  }
145
137
 
146
138
  async function report(args: ScoutArguments): Promise<number> {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { ports, type PortEnvironment } from '#ports'
4
+ import { error_text } from '#scripts/lib/error-message'
4
5
 
5
6
  const SUCCESS_EXIT_CODE = 0
6
7
  const USAGE_EXIT_CODE = 1
@@ -49,7 +50,7 @@ function run(argv: ReadonlyArray<string>, environment?: PortEnvironment): PortRe
49
50
  return { text: String(RESOLVERS[name](environment)), exit_code: SUCCESS_EXIT_CODE }
50
51
  } catch (error) {
51
52
  return {
52
- text: error instanceof Error ? error.message : String(error),
53
+ text: error_text.message_of(error),
53
54
  exit_code: FAILURE_EXIT_CODE,
54
55
  }
55
56
  }
@@ -0,0 +1,185 @@
1
+ import { adopt_toolkits } from '#scripts/adopt/adopt-toolkits'
2
+ import { propagate_run, type RunStep, type StepResult } from './propagate-run'
3
+ import { propagate_steps, type Release, type ReleasePlan } from './propagate-steps'
4
+ import { propagate_targets, type Manifest, type PropagateTarget } from './propagate-targets'
5
+
6
+ // What one `josh propagate` run carries, decided from the repository it runs in (joshuafolkken/kit#2879).
7
+ //
8
+ // The supplier is kit or any toolkit built on it — app-kit, game-kit — and a toolkit's propagation
9
+ // carries the base packages under it too, at the versions that toolkit itself has installed and was
10
+ // verified with rather than at the base's newest. Each consumer is delivered by the topmost toolkit
11
+ // it installs: a lower supplier skips it, because syncing only the base would write the overlay's
12
+ // paths back to the base's originals (`docs/sync.md` → base first, overlay after), and would hand the
13
+ // consumer a base the toolkit above it never verified. Discovery, ordering and the CLI name are
14
+ // `josh adopt`'s own reads (`scripts/adopt/adopt-toolkits.ts`), not a second copy of them.
15
+
16
+ const REFUSAL_PREFIX = 'Refusing to propagate:'
17
+ const SUPPLIER_REFUSAL = [
18
+ `${REFUSAL_PREFIX} this is not a ${adopt_toolkits.TOOLKIT_SCOPE} toolkit's own repository.`,
19
+ 'Propagation carries a published release outward, so it runs from the toolkit that published it',
20
+ '— kit, or a toolkit built on it that ships a CLI. Run it from that checkout instead.',
21
+ ].join('\n')
22
+ const UNRESOLVED_CAUSE =
23
+ 'declared here but not installed with a runnable CLI, so the consumers would receive this toolkit without it. Run `pnpm install`.'
24
+ const MISPLACED_CAUSE =
25
+ 'declared under `dependencies`, which leaves it out of the plan. Move it to `devDependencies`.'
26
+ const ARROW = ' → '
27
+ const LIST_SEPARATOR = ', '
28
+
29
+ interface Supplier {
30
+ package_name: string
31
+ bin_name: string
32
+ }
33
+
34
+ interface SupplierResolution {
35
+ supplier?: Supplier
36
+ refusal?: string
37
+ }
38
+
39
+ interface BaseResolution {
40
+ bases?: Array<Release>
41
+ cause?: string
42
+ }
43
+
44
+ interface PlanResolution {
45
+ plan?: ReleasePlan
46
+ refusal?: string
47
+ }
48
+
49
+ // A toolkit is a scoped package that ships a CLI — the same reading `josh adopt` applies to an
50
+ // installed one, applied here to the repository's own manifest.
51
+ function toolkit_name(manifest: Manifest): string | undefined {
52
+ const { name } = manifest
53
+
54
+ return name !== undefined && adopt_toolkits.is_toolkit_package(name) ? name : undefined
55
+ }
56
+
57
+ function supplier_of(manifest: Manifest | undefined): Supplier | undefined {
58
+ const package_name = manifest === undefined ? undefined : toolkit_name(manifest)
59
+ if (manifest === undefined || package_name === undefined) return undefined
60
+ const bin_name = adopt_toolkits.bin_name_of(package_name, manifest)
61
+
62
+ return bin_name === undefined ? undefined : { package_name, bin_name }
63
+ }
64
+
65
+ function resolve_supplier(project_root: string): SupplierResolution {
66
+ const supplier = supplier_of(propagate_targets.read_manifest(project_root))
67
+
68
+ return supplier === undefined ? { refusal: SUPPLIER_REFUSAL } : { supplier }
69
+ }
70
+
71
+ // A base toolkit pinned to the version installed in the supplier's own checkout — the one the
72
+ // supplier's verification gate ran against.
73
+ function pin_installed(project_root: string, release: Release): Release | undefined {
74
+ const version = propagate_targets.installed_version(project_root, release.package_name)
75
+
76
+ return version === undefined ? undefined : { ...release, version }
77
+ }
78
+
79
+ // Why a declared base toolkit would fall out of the plan, naming it — either way `josh adopt` refuses
80
+ // one for (joshuafolkken/kit#1540). A base left out is not merely left behind: every consumer that
81
+ // installs it would then read it as a toolkit above this one and be skipped.
82
+ function incomplete_cause(project_root: string): string | undefined {
83
+ const misplaced = adopt_toolkits.misplaced_toolkits(project_root)
84
+ if (misplaced.length > 0) return `${misplaced.join(LIST_SEPARATOR)} — ${MISPLACED_CAUSE}`
85
+ const unresolved = adopt_toolkits.unresolved_toolkits(project_root)
86
+
87
+ return unresolved.length === 0
88
+ ? undefined
89
+ : `${unresolved.join(LIST_SEPARATOR)} — ${UNRESOLVED_CAUSE}`
90
+ }
91
+
92
+ // The supplier's base toolkits, base first, or the names that cannot be carried.
93
+ function base_releases(project_root: string, supplier: Supplier): BaseResolution {
94
+ const cause = incomplete_cause(project_root)
95
+ if (cause !== undefined) return { cause }
96
+
97
+ const bases = adopt_toolkits
98
+ .discover_toolkits(project_root)
99
+ .filter((release) => release.package_name !== supplier.package_name)
100
+ .map((release) => pin_installed(project_root, release))
101
+
102
+ return bases.every((release) => release !== undefined) ? { bases } : { cause: UNRESOLVED_CAUSE }
103
+ }
104
+
105
+ // The releases one run carries: the base toolkits first, then the supplier at its own version, so the
106
+ // sync runs base first and the supplier's overlay is written last.
107
+ function build_plan(project_root: string, version: string): PlanResolution {
108
+ const { supplier, refusal } = resolve_supplier(project_root)
109
+ if (supplier === undefined) return { refusal: refusal ?? SUPPLIER_REFUSAL }
110
+ const { bases, cause } = base_releases(project_root, supplier)
111
+ if (bases === undefined) return { refusal: `${REFUSAL_PREFIX} ${cause ?? UNRESOLVED_CAUSE}` }
112
+ const releases = [...bases, { ...supplier, version }]
113
+
114
+ return { plan: { releases, origin: propagate_steps.PROPAGATE_ORIGIN } }
115
+ }
116
+
117
+ // The package the run publishes and waits for — by construction the plan's last release.
118
+ function supplier_name(plan: ReleasePlan): string {
119
+ return plan.releases.at(-1)?.package_name ?? ''
120
+ }
121
+
122
+ // A declared scoped package counts as a toolkit unless it is demonstrably a library: installed, and
123
+ // shipping no CLI. One that is not installed yet cannot be told apart, and treating it as a toolkit
124
+ // errs toward leaving the consumer to the propagation above rather than reverting its overlay.
125
+ function may_be_toolkit(repository_path: string, package_name: string): boolean {
126
+ if (adopt_toolkits.read_bin_name(repository_path, package_name) !== undefined) return true
127
+
128
+ return adopt_toolkits.is_unreachable_toolkit(repository_path, package_name)
129
+ }
130
+
131
+ function carriers_of(target: PropagateTarget, plan: ReleasePlan): Array<string> {
132
+ const carried = new Set(plan.releases.map((release) => release.package_name))
133
+
134
+ return adopt_toolkits
135
+ .all_declared_toolkits(target.path)
136
+ .filter((name) => !carried.has(name) && may_be_toolkit(target.path, name))
137
+ }
138
+
139
+ // A consumer that installs a toolkit this run does not carry is left to that toolkit's propagation.
140
+ // Only a candidate that would otherwise be processed or reported current is re-classified.
141
+ function mark_carried(target: PropagateTarget, plan: ReleasePlan): PropagateTarget {
142
+ if (target.state !== 'ready' && target.state !== 'up_to_date') return target
143
+ const carriers = carriers_of(target, plan)
144
+
145
+ return carriers.length === 0 ? target : { ...target, state: 'carried_above', carriers }
146
+ }
147
+
148
+ // What the dry run prints for the two steps that repeat per release: the versions each package is
149
+ // pinned to, and the sync order.
150
+ function plan_details(plan: ReleasePlan): Readonly<Record<string, string>> {
151
+ return {
152
+ [propagate_run.STEP_UPGRADE]: plan.releases
153
+ .map((release) => `${release.package_name}@${release.version}`)
154
+ .join(LIST_SEPARATOR),
155
+ [propagate_run.STEP_SYNC]: propagate_steps
156
+ .sync_commands(plan)
157
+ .map(({ command }) => command.join(' '))
158
+ .join(ARROW),
159
+ }
160
+ }
161
+
162
+ // The dry-run runner: it reports what each step would do and touches nothing.
163
+ function create_step_describer(plan: ReleasePlan): RunStep {
164
+ const details = plan_details(plan)
165
+
166
+ return (target: PropagateTarget, step: string): StepResult => {
167
+ const detail = details[step]
168
+ const suffix = detail === undefined ? '' : `: ${detail}`
169
+
170
+ console.info(` ${target.repo}: would run ${step}${suffix}`)
171
+
172
+ return { step, is_ok: true }
173
+ }
174
+ }
175
+
176
+ const propagate_plan = {
177
+ resolve_supplier,
178
+ build_plan,
179
+ supplier_name,
180
+ mark_carried,
181
+ create_step_describer,
182
+ }
183
+
184
+ export type { Supplier }
185
+ export { propagate_plan }
@@ -1,7 +1,6 @@
1
1
  import { git_gh_exec } from '#scripts/git/git-gh-exec'
2
2
  import { poll } from '#scripts/lib/poll'
3
3
  import { with_page_size } from '#scripts/version/version-remote'
4
- import { execaSync } from 'execa'
5
4
 
6
5
  // Waiting for a specific version of this package to actually exist in the registry.
7
6
  //
@@ -58,15 +57,15 @@ function finish(state: PublishWaitState, version: string, attempts: number): Pub
58
57
  // reported apart, because one means wait longer and the other means something is broken.
59
58
  function fetch_published_versions(versions_endpoint: string): Array<string> | undefined {
60
59
  const endpoint = with_page_size(versions_endpoint, VERSIONS_PAGE_SIZE)
61
- const result = execaSync('gh', ['api', endpoint, '--jq', NAMES_JQ], {
62
- ...git_gh_exec.direct_environment(),
63
- reject: false,
64
- timeout: GH_TIMEOUT_MS,
60
+ const stdout = git_gh_exec.read_gh_api_sync({
61
+ path: endpoint,
62
+ jq_filter: NAMES_JQ,
63
+ timeout_ms: GH_TIMEOUT_MS,
65
64
  })
66
- if (result.exitCode !== 0) return undefined
65
+ if (stdout === undefined) return undefined
67
66
 
68
67
  try {
69
- const parsed: unknown = JSON.parse(result.stdout)
68
+ const parsed: unknown = JSON.parse(stdout)
70
69
 
71
70
  return Array.isArray(parsed) ? parsed.filter((name) => typeof name === 'string') : undefined
72
71
  } catch {