@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
@@ -27,7 +27,7 @@ const AI_COMMANDS: Record<string, CommandEntry> = {
27
27
  reference: ['<message>', 'automation', ['git']],
28
28
  },
29
29
  epic: {
30
- script: 'scripts-ai/epic.ts',
30
+ script: 'scripts/epic/epic.ts',
31
31
  description: 'Create an epic issue from its child issue numbers',
32
32
  category: 'AI tools',
33
33
  reference: ['<title> <issue...> [--ordered]', 'automation', ['network']],
@@ -51,7 +51,7 @@ const AI_COMMANDS: Record<string, CommandEntry> = {
51
51
  reference: ['<epic>', 'automation', ['network']],
52
52
  },
53
53
  'epic:check': {
54
- script: 'scripts-ai/epic-check.ts',
54
+ script: 'scripts/epic/epic-check.ts',
55
55
  description: 'Check an epic issue against the tracking requirements',
56
56
  category: 'AI tools',
57
57
  reference: ['<epic>', 'automation', ['network']],
@@ -19,7 +19,7 @@ const BACKLOG_COMMANDS: Record<string, CommandEntry> = {
19
19
  description:
20
20
  'Collapse a backlogrun loop-head event into one call: read backlog:next, ask backlog:budget, return the verdict and any issues to start',
21
21
  category: 'AI tools',
22
- reference: ['[options]', 'automation', ['network']],
22
+ reference: ['[options]', 'automation', ['files', 'network', 'processes']],
23
23
  },
24
24
  'backlog:drive': {
25
25
  script: 'scripts/backlog/backlog-drive-cli.ts',
@@ -57,7 +57,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
57
57
  description:
58
58
  "Print each delivered rule's unaided compliance — runs reached, kept rate, refusals",
59
59
  category: 'AI tools',
60
- reference: ['', 'developer', ['none']],
60
+ reference: ['[--refresh]', 'developer', ['files']],
61
61
  },
62
62
  format: {
63
63
  script: 'scripts/lint/format.ts',
@@ -66,7 +66,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
66
66
  reference: ['', 'developer', ['files', 'processes']],
67
67
  },
68
68
  'format:edited': {
69
- script: 'scripts/hooks/format-edited-file.ts',
69
+ script: 'scripts/hooks/format-edited-cli.ts',
70
70
  description: 'Claude Code hook: format the file just edited (reads the tool call on stdin)',
71
71
  category: 'Development',
72
72
  reference: ['', 'automation', ['files', 'processes']],
@@ -85,7 +85,7 @@ const DEV_COMMANDS: Record<string, CommandEntry> = {
85
85
  // node's own `--env-file` parser with node's own precedence.
86
86
  },
87
87
  'pretool:guard': {
88
- script: 'scripts/hooks/pretool-guard.ts',
88
+ script: 'scripts/hooks/pretool-guard-cli.ts',
89
89
  description:
90
90
  'Claude Code hook: the batch, investigation and rule guards in one process (reads the tool call on stdin)',
91
91
  category: 'Development',
@@ -17,7 +17,7 @@ const PROJECT_COMMANDS: Record<string, CommandEntry> = {
17
17
  description: 'Set a project up for the GitHub Issue workflow, from git init to the setup PR',
18
18
  category: 'Project',
19
19
  reference: [
20
- `${PROFILE_ARGUMENTS} [--yes] [--github] [--public]`,
20
+ `${PROFILE_ARGUMENTS} [--yes] [--github] [--public] [--init-command <command>]`,
21
21
  'developer',
22
22
  ['files', 'git', 'network', 'processes'],
23
23
  ],
@@ -1,6 +1,6 @@
1
1
  import { OPTIONAL_ENV_FILE_FLAGS, type CommandEntry } from './josh-command-types'
2
2
 
3
- const GIT_WORKFLOW_SCRIPT = 'scripts-ai/git-workflow.ts'
3
+ const GIT_WORKFLOW_SCRIPT = 'scripts/git/git-workflow.ts'
4
4
  const GIT_MESSAGE_ARGUMENTS = '[-y] <message>'
5
5
 
6
6
  /* eslint-disable @typescript-eslint/naming-convention */
@@ -27,7 +27,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
27
27
  // session, and `followup` with them the merge step of every `fullrun` there. `doctor` was moved to
28
28
  // this form for the same reason in joshuafolkken/kit#869.
29
29
  //
30
- // Nothing is lost by relaxing it: both scripts already call `load_optional_environment()`
30
+ // Nothing is lost by relaxing it: both scripts already load `.env` through `josh_environment_file`
31
31
  // themselves, so the file is read either way and node's precedence — an existing environment
32
32
  // variable wins over the file — is node's own in both spellings.
33
33
  //
@@ -35,7 +35,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
35
35
  // than dropped: `telegram_notify.send` now throws on missing credentials and on a failed send, so
36
36
  // `josh notify` exits non-zero instead of warning and answering 0.
37
37
  followup: {
38
- script: 'scripts-ai/git-followup-workflow.ts',
38
+ script: 'scripts/git/git-followup-workflow.ts',
39
39
  description: 'Follow-up git workflow',
40
40
  category: 'Workflow',
41
41
  reference: [
@@ -46,7 +46,7 @@ const WORKFLOW_COMMANDS: Record<string, CommandEntry> = {
46
46
  tsx_arguments: OPTIONAL_ENV_FILE_FLAGS,
47
47
  },
48
48
  notify: {
49
- script: 'scripts-ai/telegram-test.ts',
49
+ script: 'scripts/git/telegram-test.ts',
50
50
  description: 'Send Telegram notification',
51
51
  category: 'Workflow',
52
52
  reference: ['--task-type <type> --body <text>', 'automation', ['notifications']],
@@ -19,6 +19,15 @@ import { lane_paths } from './lane-paths'
19
19
  // `.claude/skills/workflow-commands/pre-gate-cut.md`.
20
20
  const KEY = 'JOSH_LANE_CHILD'
21
21
 
22
+ // **A dispatched child also asks for the five-minute prompt-cache TTL** (joshuafolkken/kit#2944). A
23
+ // headless `claude -p` session on a subscription defaults to the one-hour TTL, whose writes bill at 2x
24
+ // the input rate against 1.25x for five minutes. A replay of 1,953 lane transcripts (about 57,000
25
+ // requests) found 0.3% of request gaps over five minutes, so the shorter TTL — re-writing the prefix on
26
+ // those few gaps — still costs 12-13% less. Every launcher of a child composes its environment here,
27
+ // so this is the one place the choice is made.
28
+ const CACHE_TTL_KEY = 'CLAUDE_CODE_PROMPT_CACHE_TTL'
29
+ const CACHE_TTL = '5m'
30
+
22
31
  type MarkerSource = Readonly<Record<string, string | undefined>>
23
32
 
24
33
  // The environment fragment that marks a child dispatched for `#<issue>`. Composed from a digits-only
@@ -27,7 +36,7 @@ type MarkerSource = Readonly<Record<string, string | undefined>>
27
36
  function environment_for(issue: string): Record<string, string> {
28
37
  run_issue_number.require_issue_number(issue)
29
38
 
30
- return { [KEY]: issue }
39
+ return { [KEY]: issue, [CACHE_TTL_KEY]: CACHE_TTL }
31
40
  }
32
41
 
33
42
  // The issue a child was dispatched for, or `undefined` when the mark is absent or malformed. A blank
@@ -52,7 +61,12 @@ function is_child_of(directory: string, source: MarkerSource = process.env): boo
52
61
  return marked !== undefined && marked === lane_paths.lane_issue_of(directory, { ...source })
53
62
  }
54
63
 
55
- const lane_child_marker = { KEY, env_for: environment_for, is_child_of, marked_issue }
64
+ const lane_child_marker = {
65
+ KEY,
66
+ env_for: environment_for,
67
+ is_child_of,
68
+ marked_issue,
69
+ }
56
70
 
57
71
  export type { MarkerSource }
58
72
  export { lane_child_marker }
@@ -2,6 +2,9 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { epic_busy } from '#scripts/epic/epic-busy'
4
4
  import { git_gh_command } from '#scripts/git/git-gh-command'
5
+ import type { OpenIssueData } from '#scripts/git/schemas'
6
+ import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
7
+ import { error_text } from '#scripts/lib/error-message'
5
8
  import { run_carry } from '#scripts/run/run-carry'
6
9
  import { lane_await } from './lane-await'
7
10
  import { lane_close, type CloseOutcome, type SweepOutcome } from './lane-close'
@@ -149,10 +152,10 @@ function report_sweep(outcome: SweepOutcome): number {
149
152
 
150
153
  // `none` on standard output for the empty case as well, so `lane:list` keeps the contract the rest
151
154
  // of the commands follow — the prose belongs on standard error with every other explanation.
152
- function report_lanes(lanes: ReadonlyArray<LaneInfo>): number {
155
+ function report_lanes(lanes: ReadonlyArray<LaneInfo>, cite: IssueCiter): number {
153
156
  if (lanes.length === 0) console.error(lane_report.NO_LANES)
154
157
 
155
- console.info(lanes.length === 0 ? NONE_TOKEN : lane_report.describe_lanes(lanes))
158
+ console.info(lanes.length === 0 ? NONE_TOKEN : lane_report.describe_lanes(lanes, cite))
156
159
 
157
160
  return SUCCESS_EXIT_CODE
158
161
  }
@@ -191,23 +194,39 @@ const OCCUPANCY_UNREADABLE =
191
194
  // The `in-progress` issues this repository shows running, read the way `epic:next` reads lane
192
195
  // occupancy — never rebuilt (joshuafolkken/kit#2235). A read that could not see the whole listing is
193
196
  // `undefined`, so the difference is skipped rather than computed against a set known to be partial.
194
- async function to_holder_numbers(repo: string): Promise<ReadonlyArray<number> | undefined> {
197
+ async function to_holders(repo: string): Promise<ReadonlyArray<OpenIssueData> | undefined> {
195
198
  const read = await epic_busy.read_repository(repo)
196
199
  if (read.kind === 'idle') return []
197
200
 
198
- return read.kind === 'busy' ? read.issues.map((issue) => issue.number) : undefined
201
+ return read.kind === 'busy' ? read.issues : undefined
199
202
  }
200
203
 
201
- async function read_in_progress(): Promise<ReadonlyArray<number> | undefined> {
204
+ // The repository and its running issues, read once: the listing cites each lane from the titles here
205
+ // (joshuafolkken/kit#2943) and the occupancy check compares against the same set.
206
+ interface InProgress {
207
+ repo: string | undefined
208
+ holders: ReadonlyArray<OpenIssueData> | undefined
209
+ }
210
+
211
+ async function read_in_progress(): Promise<InProgress> {
202
212
  try {
203
213
  const repo = await git_gh_command.repo_get_name_with_owner()
204
214
 
205
- return repo === undefined ? undefined : await to_holder_numbers(repo)
215
+ return { repo, holders: repo === undefined ? undefined : await to_holders(repo) }
206
216
  } catch {
207
- return undefined
217
+ return { repo: undefined, holders: undefined }
208
218
  }
209
219
  }
210
220
 
221
+ function citer_of(in_progress: InProgress): IssueCiter {
222
+ const holders = in_progress.holders ?? []
223
+
224
+ return issue_cite.citer(
225
+ in_progress.repo,
226
+ new Map(holders.map((issue) => [String(issue.number), issue.title])),
227
+ )
228
+ }
229
+
211
230
  // A stranded lane's work tree is gone, so it holds no issue: it counts as no lane rather than a live
212
231
  // one, which is what turns its surviving `in-progress` label into a `stopped` anomaly.
213
232
  function live_lane_issues(lanes: ReadonlyArray<LaneInfo>): Array<number> {
@@ -216,16 +235,15 @@ function live_lane_issues(lanes: ReadonlyArray<LaneInfo>): Array<number> {
216
235
 
217
236
  // The lane/label difference, printed to standard error beside the listing. Wrapped so a GitHub read
218
237
  // that fails skips only this section — `lane:list`'s job is to list the lanes it already read.
219
- async function report_occupancy(lanes: ReadonlyArray<LaneInfo>): Promise<void> {
220
- const in_progress = await read_in_progress()
221
-
222
- if (in_progress === undefined) {
238
+ function report_occupancy(lanes: ReadonlyArray<LaneInfo>, in_progress: InProgress): void {
239
+ if (in_progress.holders === undefined) {
223
240
  console.error(OCCUPANCY_UNREADABLE)
224
241
 
225
242
  return
226
243
  }
227
244
 
228
- const report = lane_occupancy.classify(in_progress, {
245
+ const holders = in_progress.holders.map((issue) => issue.number)
246
+ const report = lane_occupancy.classify(holders, {
229
247
  kind: 'lanes',
230
248
  issues: live_lane_issues(lanes),
231
249
  })
@@ -238,9 +256,10 @@ async function list_command(rest: ReadonlyArray<string>): Promise<number> {
238
256
  if (rest.length > 0) return report_usage()
239
257
 
240
258
  const lanes = await lane_registry.list_lanes()
241
- const code = report_lanes(lanes)
259
+ const in_progress = await read_in_progress()
260
+ const code = report_lanes(lanes, citer_of(in_progress))
242
261
 
243
- await report_occupancy(lanes)
262
+ report_occupancy(lanes, in_progress)
244
263
 
245
264
  return code
246
265
  }
@@ -384,7 +403,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
384
403
  try {
385
404
  return await dispatch(argv)
386
405
  } catch (error) {
387
- console.error(error instanceof Error ? error.message : String(error))
406
+ console.error(error_text.message_of(error))
388
407
 
389
408
  return FAILURE_EXIT_CODE
390
409
  }
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { ENV_FILE_NAME } from '#ports'
4
4
  import { git_worktree } from '#scripts/git/git-worktree'
5
+ import { file_reader } from '#scripts/lib/read-file'
5
6
  import { lane_cache } from './lane-cache'
6
7
  import { lane_environment } from './lane-environment'
7
8
  import { lane_install, type InstallResult } from './lane-install'
@@ -52,7 +53,7 @@ const SEAT_LOCK_PREFIX = 'seat-'
52
53
  function read_root_environment(repository_root: string): string {
53
54
  const file = path.join(repository_root, ENV_FILE_NAME)
54
55
 
55
- return existsSync(file) ? readFileSync(file, 'utf8') : ''
56
+ return file_reader.read_file_or_empty(file)
56
57
  }
57
58
 
58
59
  function build_lane(
@@ -1,10 +1,11 @@
1
- import { existsSync, readFileSync } from 'node:fs'
1
+ import { existsSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { ENV_FILE_NAME } from '#ports'
4
4
  import type { AgentProfile } from '#scripts/agent/agent-role-profile'
5
5
  import { repo_discovery } from '#scripts/discovery/repo-discovery'
6
6
  import { git_command } from '#scripts/git/git-command'
7
7
  import { git_worktree } from '#scripts/git/git-worktree'
8
+ import { file_reader } from '#scripts/lib/read-file'
8
9
  import { lane_environment } from './lane-environment'
9
10
  import { lane_paths } from './lane-paths'
10
11
 
@@ -105,11 +106,7 @@ function held_seat(content: string | undefined): SeatPorts {
105
106
  * a state the report shows rather than a state anything falls back from.
106
107
  */
107
108
  function read_environment(directory: string): string | undefined {
108
- try {
109
- return readFileSync(path.join(directory, ENV_FILE_NAME), 'utf8')
110
- } catch {
111
- return undefined
112
- }
109
+ return file_reader.read_if_readable(path.join(directory, ENV_FILE_NAME))
113
110
  }
114
111
 
115
112
  // A lane is identified by its branch rather than by where it sits, so nothing here compares two
@@ -1,4 +1,5 @@
1
1
  import { agent_role_profile } from '#scripts/agent/agent-role-profile'
2
+ import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
2
3
  import { run_liveness } from '#scripts/run/run-liveness'
3
4
  import type { LaneInfo } from './lane-registry'
4
5
 
@@ -48,9 +49,14 @@ function event_column(lane: LaneInfo): string {
48
49
  return state ?? 'agent -'
49
50
  }
50
51
 
51
- function describe_lane(lane: LaneInfo): string {
52
+ // The plain `#N`, for a caller that could not read the repository the issue would link into.
53
+ const PLAIN_CITE: IssueCiter = issue_cite.citer(undefined, new Map())
54
+
55
+ // The issue leads as a citation (joshuafolkken/kit#2943): a lane status report copies this row, and a
56
+ // bare `#N` copied into it is what the Stop guard sends back.
57
+ function describe_lane(lane: LaneInfo, cite: IssueCiter = PLAIN_CITE): string {
52
58
  return [
53
- `#${lane.issue}`,
59
+ cite(lane.issue),
54
60
  `seat ${value_or_unknown(lane.seat)}`,
55
61
  `dev ${value_or_unknown(lane.development_port)}`,
56
62
  `preview ${value_or_unknown(lane.preview_port)}`,
@@ -63,10 +69,10 @@ function describe_lane(lane: LaneInfo): string {
63
69
  ].join(COLUMN_SEPARATOR)
64
70
  }
65
71
 
66
- function describe_lanes(lanes: ReadonlyArray<LaneInfo>): string {
72
+ function describe_lanes(lanes: ReadonlyArray<LaneInfo>, cite: IssueCiter = PLAIN_CITE): string {
67
73
  if (lanes.length === 0) return NO_LANES
68
74
 
69
- return lanes.map((lane) => describe_lane(lane)).join('\n')
75
+ return lanes.map((lane) => describe_lane(lane, cite)).join('\n')
70
76
  }
71
77
 
72
78
  function describe_ports(lane: LaneInfo): string {
@@ -1,4 +1,6 @@
1
- // Argument refusal for the small flag-only commands.
1
+ import { parseArgs, type ParseArgsConfig } from 'node:util'
2
+
3
+ // Argument reading for the commands under `scripts/`.
2
4
  //
3
5
  // Reject anything not on the list rather than ignoring it: a misspelled `--dryrun` that fell through
4
6
  // would run the real write path. `josh propagate` and `josh adopt` both write into working trees, so
@@ -16,4 +18,20 @@ function refuse_unknown_flags(
16
18
  return `Unknown argument(s): ${unknown.join(' ')}\n${usage}`
17
19
  }
18
20
 
19
- export { refuse_unknown_flags }
21
+ // `parseArgs`, answering `undefined` where it would throw — an unknown flag, a missing option value,
22
+ // or a positional the config does not allow. Each caller turns that into its usage line and a failure
23
+ // exit rather than a stack trace, on commands whose output a workflow reads; eleven of them had begun
24
+ // to carry a copy of the same try/catch each (joshuafolkken/kit#2902), and this is the one copy.
25
+ function parse_or_undefined<T extends ParseArgsConfig>(
26
+ config: T,
27
+ ): ReturnType<typeof parseArgs<T>> | undefined {
28
+ try {
29
+ return parseArgs(config)
30
+ } catch {
31
+ return undefined
32
+ }
33
+ }
34
+
35
+ const cli_flags = { refuse_unknown_flags, parse_or_undefined }
36
+
37
+ export { cli_flags }
@@ -0,0 +1,9 @@
1
+ // The one place a caught value becomes the text a report shows: an `Error` contributes its own message,
2
+ // anything else thrown is stringified as it stands.
3
+ function message_of(error: unknown): string {
4
+ return error instanceof Error ? error.message : String(error)
5
+ }
6
+
7
+ const error_text = { message_of }
8
+
9
+ export { error_text }
@@ -1,16 +1,40 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
 
3
+ const ABSENT_CODE = 'ENOENT'
4
+
5
+ function is_absent(error: unknown): boolean {
6
+ return error instanceof Error && 'code' in error && error.code === ABSENT_CODE
7
+ }
8
+
9
+ // An absent file is an answer ("nothing declared here"); any other failure — a permission error, a
10
+ // directory where a file was expected — is not, and is thrown so it cannot pass for an empty file.
11
+ function read_optional(path: string): string | undefined {
12
+ try {
13
+ return readFileSync(path, 'utf8')
14
+ } catch (error) {
15
+ if (is_absent(error)) return undefined
16
+
17
+ throw error
18
+ }
19
+ }
20
+
3
21
  // Several readers treat an absent file as "nothing declared" rather than an error: a project may
4
22
  // carry no lockfile yet, and either overrides location may be missing entirely. Empty content is
5
23
  // the shared stand-in, so the callers do not each re-implement the try/catch.
6
24
  function read_file_or_empty(path: string): string {
25
+ return read_optional(path) ?? ''
26
+ }
27
+
28
+ // For a reader whose contract is to never fail — a hook running after a write already succeeded, a
29
+ // report that states "unreadable" itself — every failure to read, not only absence, is `undefined`.
30
+ function read_if_readable(path: string): string | undefined {
7
31
  try {
8
32
  return readFileSync(path, 'utf8')
9
33
  } catch {
10
- return ''
34
+ return undefined
11
35
  }
12
36
  }
13
37
 
14
- const file_reader = { read_file_or_empty }
38
+ const file_reader = { read_optional, read_file_or_empty, read_if_readable }
15
39
 
16
40
  export { file_reader }
@@ -1,40 +1,105 @@
1
- import { appendFile, mkdir } from 'node:fs/promises'
1
+ import { appendFile, mkdir, readdir, readFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
- import { repo_discovery } from '#scripts/discovery/repo-discovery'
4
- import { OBSERVATION_LEDGER_PATH } from './observation-ledger'
3
+ import { git_branch } from '#scripts/git/git-branch'
4
+ import { git_spawn } from '#scripts/git/git-spawn'
5
+ import { observation_ledger, OBSERVATION_LEDGER_DIRECTORY } from './observation-ledger'
5
6
  import { observation_ledger_migrate } from './observation-ledger-migrate'
6
7
 
7
- // Where the observation ledger is read and written, whichever work tree a command runs in
8
- // (joshuafolkken/kit#2419).
8
+ // Where the observation ledger is read and written, whichever work tree a command runs in.
9
9
  //
10
- // **The ledger lives in the primary checkout, never in a lane.** A lane is a linked work tree that is
11
- // closed once its branch merges, and the ledger is excluded from ordinary staging — so a line appended
12
- // in a lane had no route to the default branch: `pnpm josh followup`'s flush needs `pnpm josh ms`,
13
- // which a lane refuses, and `lane:close` does not carry the file. Resolving the ledger to the primary
14
- // checkout at every reader and writer puts the line where the flush already works, rather than adding
15
- // a second route that moves it there afterwards.
16
- //
17
- // The primary checkout is `repo_discovery.main_worktree`'s answer, reused rather than spelled again;
18
- // in an ordinary checkout it is the given directory itself, so nothing outside a lane changes.
10
+ // **The ledger lives in the work tree the command runs in — a lane's in a lane** (joshuafolkken/kit#2919).
11
+ // It used to resolve to the primary checkout from every lane (joshuafolkken/kit#2419), so a lane's
12
+ // lines waited there for a batch-end flush; another run's stash of that checkout took them, the flush
13
+ // read `git status` and found nothing, and the lines were lost. Each run now appends to its own
14
+ // issue's file in its own tree, `git-staging.ts` commits it with the run, and `pnpm josh followup`
15
+ // commits whatever was appended after that commit before it merges — so a lane's lines reach the
16
+ // default branch with the lane's own pull request, and the primary checkout holds none of them.
19
17
 
20
- function ledger_root(cwd: string = process.cwd()): string {
21
- return repo_discovery.main_worktree(cwd)
22
- }
18
+ const DATE_END = 10
23
19
 
24
- function is_lane(cwd: string = process.cwd()): boolean {
25
- return path.resolve(ledger_root(cwd)) !== path.resolve(cwd)
20
+ function ledger_root(cwd: string = process.cwd()): string {
21
+ return cwd
26
22
  }
27
23
 
28
- // **Resolving the path migrates the ledger first** (joshuafolkken/kit#2724). Every reader and writer
29
- // asks here — `review:record --check`, `review:findings`, the retrospective, the stash carry's
30
- // duplicate check — so none of them can read an empty new ledger while the lines still sit at the old
31
- // path, which for the review-record check would answer `not-required` for a round never recorded.
32
- function ledger_path(cwd: string = process.cwd()): string {
24
+ // **Resolving the directory migrates the single-file ledgers first** (joshuafolkken/kit#2724,
25
+ // joshuafolkken/kit#2919). Every reader and writer asks here — `review:record --check`,
26
+ // `review:findings`, the retrospective, the stash carry's duplicate check — so none of them can read a
27
+ // directory without the lines that still sit at an old path, which for the review-record check would
28
+ // answer `missing` for a round recorded before the move.
29
+ function ledger_directory(cwd: string = process.cwd()): string {
33
30
  const root = ledger_root(cwd)
34
31
 
35
32
  observation_ledger_migrate.migrate(root)
36
33
 
37
- return path.join(root, OBSERVATION_LEDGER_PATH)
34
+ return path.join(root, OBSERVATION_LEDGER_DIRECTORY)
35
+ }
36
+
37
+ function file_path(name: number | string, cwd: string): string {
38
+ ledger_directory(cwd)
39
+
40
+ return path.join(ledger_root(cwd), observation_ledger.ledger_file(name))
41
+ }
42
+
43
+ function issue_path(issue: number, cwd: string = process.cwd()): string {
44
+ return file_path(issue, cwd)
45
+ }
46
+
47
+ async function current_branch(cwd: string): Promise<string> {
48
+ try {
49
+ return await git_spawn.read(['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'])
50
+ } catch {
51
+ return ''
52
+ }
53
+ }
54
+
55
+ // **The file a writer that names no issue appends to** — the issue the checked-out branch leads with
56
+ // (`2919-lane`, `2919-store-the-ledger`), so a lane's lines land in its own issue's file; or the date,
57
+ // for a line written on the default branch outside any issue's run, which `pnpm josh
58
+ // observations:flush` commits.
59
+ async function writer_path(now: Date, cwd: string = process.cwd()): Promise<string> {
60
+ const issue = git_branch.issue_from_branch(await current_branch(cwd))
61
+
62
+ return file_path(issue ?? now.toISOString().slice(0, DATE_END), cwd)
63
+ }
64
+
65
+ // A file whose last line has no newline would otherwise run into the next file's first line.
66
+ async function read_file(target: string): Promise<string> {
67
+ try {
68
+ const content = await readFile(target, 'utf8')
69
+
70
+ return content.length === 0 || content.endsWith('\n') ? content : `${content}\n`
71
+ } catch {
72
+ return ''
73
+ }
74
+ }
75
+
76
+ async function ledger_files(directory: string): Promise<ReadonlyArray<string> | undefined> {
77
+ try {
78
+ const names = await readdir(directory)
79
+
80
+ return names
81
+ .filter((name) => observation_ledger.is_ledger_file_name(name))
82
+ .toSorted((left, right) => left.localeCompare(right))
83
+ } catch {
84
+ return undefined
85
+ }
86
+ }
87
+
88
+ // **Every file in the directory, read as one ledger**, in name order so the answer is stable. Readers
89
+ // count across all of them — a recurrence is a recurrence whichever issue's file each line sits in.
90
+ // `undefined` when there is no directory: a checkout that keeps no ledger, which `review:record
91
+ // --check` turns into `not-required`.
92
+ async function read(cwd: string = process.cwd()): Promise<string | undefined> {
93
+ const directory = ledger_directory(cwd)
94
+ const files = await ledger_files(directory)
95
+
96
+ if (files === undefined) return undefined
97
+
98
+ const contents = await Promise.all(
99
+ files.map(async (name) => await read_file(path.join(directory, name))),
100
+ )
101
+
102
+ return contents.join('')
38
103
  }
39
104
 
40
105
  // The one append every ledger writer goes through. A consumer that does not keep the ledger has no
@@ -47,6 +112,13 @@ async function append(target: string, lines: ReadonlyArray<string>): Promise<voi
47
112
  await appendFile(target, `${lines.join('\n')}\n`, 'utf8')
48
113
  }
49
114
 
50
- const observation_ledger_home = { append, is_lane, ledger_path, ledger_root }
115
+ const observation_ledger_home = {
116
+ append,
117
+ issue_path,
118
+ ledger_directory,
119
+ ledger_root,
120
+ read,
121
+ writer_path,
122
+ }
51
123
 
52
124
  export { observation_ledger_home }
@@ -1,18 +1,20 @@
1
1
  import { appendFileSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { file_reader } from '#scripts/lib/read-file'
3
4
  import {
4
- LEGACY_OBSERVATION_LEDGER_PATH,
5
+ LEGACY_LEDGER_FILE,
6
+ LEGACY_OBSERVATION_LEDGER_PATHS,
5
7
  MIGRATION_CLAIM_SUFFIX,
6
- OBSERVATION_LEDGER_PATH,
8
+ OBSERVATION_LEDGER_DIRECTORY,
7
9
  } from './observation-ledger'
8
10
  import { observation_ledger_line } from './observation-ledger-line'
9
11
 
10
- // Moves the ledger's lines from the path it had before joshuafolkken/kit#2724 to the one it has now
11
- // (`observation-ledger.ts` carries why both are recognized). Changing the constant alone split the
12
- // appends across two files: a run still on the old code recreated `docs/observations.md`, and a
13
- // consumer repository's existing ledger stayed where nothing read it. So resolving the ledger's path
14
- // migrates first (`observation-ledger-home.ts`), and whatever reached the old path lands at the tail
15
- // of the new one before anything reads or appends.
12
+ // Moves the ledger's lines from the single files it was before joshuafolkken/kit#2919 into the
13
+ // directory it is now, as `legacy.md` (`observation-ledger.ts` carries why the old paths are still
14
+ // recognized). Changing a constant alone splits the appends: a run still on the old code recreates the
15
+ // old file, and a consumer repository's existing ledger stays where nothing reads it. So resolving the
16
+ // ledger migrates first (`observation-ledger-home.ts`), and whatever reached an old path lands in the
17
+ // directory before anything reads or appends.
16
18
  //
17
19
  // **The old file is claimed with a rename before it is read.** Two sessions resolving the ledger at
18
20
  // once would otherwise both read it and both copy its lines — and a repeated line under one key is
@@ -83,14 +85,6 @@ function terminated(content: string): string {
83
85
  return content.length === 0 || content.endsWith(NEWLINE) ? content : `${content}${NEWLINE}`
84
86
  }
85
87
 
86
- function read_or_undefined(file_path: string): string | undefined {
87
- try {
88
- return readFileSync(file_path, 'utf8')
89
- } catch {
90
- return undefined
91
- }
92
- }
93
-
94
88
  // What of the claimed file goes to the ledger's tail: all of it, verbatim, when there is no ledger yet,
95
89
  // and otherwise only the lines the ledger does not already hold. **The old file can come back after
96
90
  // its lines were moved** — `git restore`, `git reset --hard` or a stash without `-u` restores a
@@ -108,7 +102,11 @@ function carried(content: string, ledger: string | undefined): string {
108
102
  // Moves one claimed file's lines to the tail of the ledger, then removes the claim.
109
103
  function absorb(claimed: string, target: string): void {
110
104
  mkdirSync(path.dirname(target), { recursive: true })
111
- appendFileSync(target, carried(readFileSync(claimed, 'utf8'), read_or_undefined(target)), 'utf8')
105
+ appendFileSync(
106
+ target,
107
+ carried(readFileSync(claimed, 'utf8'), file_reader.read_if_readable(target)),
108
+ 'utf8',
109
+ )
112
110
  rmSync(claimed)
113
111
  }
114
112
 
@@ -127,10 +125,7 @@ function absorb_stale(legacy: string, target: string): number {
127
125
  return count
128
126
  }
129
127
 
130
- // Returns whether anything was moved. `root` is the checkout the ledger lives in.
131
- function migrate(root: string): boolean {
132
- const legacy = path.join(root, LEGACY_OBSERVATION_LEDGER_PATH)
133
- const target = path.join(root, OBSERVATION_LEDGER_PATH)
128
+ function migrate_one(legacy: string, target: string): boolean {
134
129
  const recovered = absorb_stale(legacy, target)
135
130
  const claimed = claim_path(legacy)
136
131
 
@@ -141,6 +136,16 @@ function migrate(root: string): boolean {
141
136
  return true
142
137
  }
143
138
 
139
+ // Returns whether anything was moved. `root` is the checkout the ledger lives in. Every old path is
140
+ // tried — `map` before `some`, so one that moved does not leave the next one where it was.
141
+ function migrate(root: string): boolean {
142
+ const target = path.join(root, OBSERVATION_LEDGER_DIRECTORY, LEGACY_LEDGER_FILE)
143
+
144
+ return LEGACY_OBSERVATION_LEDGER_PATHS.map((legacy) =>
145
+ migrate_one(path.join(root, legacy), target),
146
+ ).some(Boolean)
147
+ }
148
+
144
149
  const observation_ledger_migrate = { claim_path, migrate }
145
150
 
146
151
  export { observation_ledger_migrate }