@joshuafolkken/kit 1.1010.0 → 1.1035.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 (272) 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/workflow-commands/SKILL.md +8 -23
  5. package/.claude/skills/workflow-commands/backlogrun-child.md +4 -2
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -12
  7. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
  8. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  10. package/.claude/skills/workflow-commands/into-target.md +1 -1
  11. package/.claude/skills/workflow-commands/observation-filing.md +42 -43
  12. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  13. package/.codex/hooks.json +1 -5
  14. package/.github/workflows/sonar-qube.yml +1 -1
  15. package/AGENTS.md +4 -25
  16. package/CLAUDE.md +24 -38
  17. package/GEMINI.md +4 -24
  18. package/README.md +22 -56
  19. package/dist/CLAUDE.md +24 -38
  20. package/dist/hooks/batch-guard.js +4 -4
  21. package/dist/hooks/{chunk-WMIR5SNX.js → chunk-D67KVGGY.js} +3 -3
  22. package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-EIFCAJRB.js} +70 -27
  23. package/dist/hooks/{chunk-KPZN42RT.js → chunk-GJT4BNVE.js} +3 -3
  24. package/dist/hooks/{chunk-PAXMLGSS.js → chunk-GLBDBJOK.js} +44 -24
  25. package/dist/hooks/{chunk-GBJUERYK.js → chunk-HOYJU5RM.js} +1 -1
  26. package/dist/hooks/{chunk-6PSTVOLH.js → chunk-HU66PWWL.js} +33 -57
  27. package/dist/hooks/{chunk-A2H5I2VD.js → chunk-KG6PZD72.js} +2 -2
  28. package/dist/hooks/{chunk-AWTFKKFW.js → chunk-XPLP27WA.js} +3 -8
  29. package/dist/hooks/{chunk-4GDR2YTV.js → chunk-Y27M6A7X.js} +4 -4
  30. package/dist/hooks/{chunk-56LGMJJV.js → chunk-ZXNE6NY3.js} +2 -2
  31. package/dist/hooks/codex-hook-adapter.js +10 -10
  32. package/dist/hooks/duplicate-read-guard.js +5 -5
  33. package/dist/hooks/format-edited.js +2 -2
  34. package/dist/hooks/investigation-guard.js +5 -5
  35. package/dist/hooks/pretool-guard.js +9 -9
  36. package/dist/hooks/stop-guard.js +11 -15
  37. package/dist/josh.js +38 -19
  38. package/dist/version/index.js +93 -41
  39. package/eslint/base.js +2 -11
  40. package/package.json +31 -5
  41. package/prompts/coding-standards.md +15 -290
  42. package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
  43. package/prompts/collaboration-workflow/epic-audit.md +2 -53
  44. package/prompts/collaboration-workflow/epic-bundle.md +2 -106
  45. package/prompts/collaboration-workflow/issue-template.md +4 -4
  46. package/prompts/collaboration-workflow/operating-rules.md +18 -75
  47. package/prompts/collaboration-workflow/principles.md +74 -0
  48. package/prompts/collaboration-workflow/residency.md +54 -14
  49. package/prompts/collaboration-workflow/rule-delivery.md +1 -11
  50. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  51. package/prompts/collaboration-workflow.md +22 -28
  52. package/scripts/adopt/adopt-logic.ts +2 -2
  53. package/scripts/adopt/adopt-toolkits.ts +1 -1
  54. package/scripts/adopt/adopt.ts +2 -2
  55. package/scripts/agent/agent-event.ts +2 -9
  56. package/scripts/agent/agent-exit-record.ts +2 -9
  57. package/scripts/backlog/backlog-budget-cli.ts +2 -6
  58. package/scripts/backlog/backlog-drive-cli.ts +20 -10
  59. package/scripts/backlog/backlog-drive.ts +50 -16
  60. package/scripts/backlog/backlog-offer-cli.ts +6 -5
  61. package/scripts/backlog/backlog-plan.ts +1 -1
  62. package/scripts/build/build-bin.ts +2 -1
  63. package/scripts/build/build-config-merge.ts +2 -1
  64. package/scripts/build/build-hooks.ts +24 -4
  65. package/scripts/build/build-managed-marker.ts +2 -1
  66. package/scripts/build/build-self-sync-guard.ts +2 -1
  67. package/scripts/build/build-version.ts +2 -1
  68. package/scripts/cases/cases-logic.ts +2 -6
  69. package/scripts/ci/pr-classification.ts +13 -11
  70. package/scripts/cost-runtime/cost-transcript.ts +4 -11
  71. package/scripts/doctor/doctor-consumer.ts +5 -12
  72. package/scripts/doctor/doctor-ports.ts +2 -2
  73. package/scripts/document/document-byte-budget.ts +8 -16
  74. package/scripts/document/document-section.ts +2 -6
  75. package/scripts/document/entry-read-budget.ts +1 -1
  76. package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
  77. package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
  78. package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
  79. package/scripts/epic/epic-audit-cli.ts +4 -4
  80. package/scripts/epic/epic-audit-orphans.ts +2 -2
  81. package/scripts/epic/epic-audit-rationale.ts +1 -1
  82. package/scripts/epic/epic-audit.ts +1 -1
  83. package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
  84. package/scripts/epic/epic-bundle-cli.ts +1 -1
  85. package/scripts/epic/epic-bundle.ts +2 -2
  86. package/scripts/epic/epic-candidate-confirm.ts +2 -1
  87. package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
  88. package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
  89. package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
  90. package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
  91. package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
  92. package/scripts/epic/epic-cross-repo.ts +21 -38
  93. package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
  94. package/scripts/epic/epic-fetch.ts +3 -3
  95. package/scripts/epic/epic-graph.ts +3 -3
  96. package/scripts/epic/epic-index.ts +2 -2
  97. package/scripts/epic/epic-issue.ts +1 -1
  98. package/scripts/epic/epic-next.ts +3 -3
  99. package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
  100. package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
  101. package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
  102. package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
  103. package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
  104. package/scripts/epic/epic-relation-recheck.ts +4 -3
  105. package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
  106. package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
  107. package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
  108. package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
  109. package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
  110. package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
  111. package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
  112. package/{scripts-ai → scripts/epic}/epic.ts +17 -16
  113. package/scripts/gh/fix-gh-packages.ts +2 -1
  114. package/scripts/gh/gh-spawn.ts +12 -10
  115. package/scripts/git/constants.ts +1 -1
  116. package/scripts/git/git-branch.ts +10 -1
  117. package/scripts/git/git-command.ts +1 -1
  118. package/scripts/git/git-error.ts +2 -5
  119. package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
  120. package/scripts/git/git-followup-flush.ts +74 -48
  121. package/scripts/git/git-followup-issue-close.ts +2 -2
  122. package/scripts/git/git-followup-label.ts +1 -1
  123. package/scripts/git/git-followup-pending.ts +1 -1
  124. package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
  125. package/scripts/git/git-gh-exec.ts +85 -26
  126. package/scripts/git/git-gh-issue-read.ts +2 -2
  127. package/scripts/git/git-gh-issue-rest.ts +2 -2
  128. package/scripts/git/git-gh-issue-write.ts +6 -6
  129. package/scripts/git/git-gh-pr-read.ts +0 -21
  130. package/scripts/git/git-gh-pr.ts +2 -23
  131. package/scripts/git/git-pr-followup-wrapup.ts +2 -6
  132. package/scripts/git/git-preflight.ts +3 -6
  133. package/scripts/git/git-staging.ts +21 -9
  134. package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
  135. package/scripts/git/github-issue-url.ts +1 -1
  136. package/scripts/git/issue-labels.ts +1 -1
  137. package/scripts/git/issue-reference.ts +19 -0
  138. package/scripts/git/main-merge.ts +2 -1
  139. package/scripts/git/main-sync.ts +3 -2
  140. package/scripts/git/schemas.ts +1 -1
  141. package/scripts/git/stash-pop-args.ts +7 -13
  142. package/scripts/git/stash-pop-cli.ts +2 -1
  143. package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
  144. package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
  145. package/scripts/hooks/format-rewrite-notice.ts +2 -8
  146. package/scripts/init/init-ai-copy.ts +2 -5
  147. package/scripts/init/init.ts +8 -4
  148. package/scripts/init/start-steps.ts +2 -1
  149. package/scripts/init/start.ts +2 -1
  150. package/scripts/issue/issue-comment-cli.ts +2 -1
  151. package/scripts/issue/issue-file-cli.ts +2 -1
  152. package/scripts/issue/issue-fold-cli.ts +9 -16
  153. package/scripts/issue/issue-scout-cli.ts +8 -13
  154. package/scripts/issue/measure-rerun-cli.ts +7 -6
  155. package/scripts/josh/josh-commands-ai.ts +2 -2
  156. package/scripts/josh/josh-commands-backlog.ts +1 -1
  157. package/scripts/josh/josh-commands-development.ts +1 -1
  158. package/scripts/josh/josh-commands-workflow.ts +4 -4
  159. package/scripts/lane/lane-cli.ts +2 -1
  160. package/scripts/lane/lane-open.ts +3 -2
  161. package/scripts/lane/lane-registry.ts +3 -6
  162. package/scripts/lib/cli-flags.ts +20 -2
  163. package/scripts/lib/error-message.ts +9 -0
  164. package/scripts/lib/read-file.ts +26 -2
  165. package/scripts/observations/observation-ledger-home.ts +98 -26
  166. package/scripts/observations/observation-ledger-migrate.ts +26 -21
  167. package/scripts/observations/observation-ledger-prepare.ts +10 -20
  168. package/scripts/observations/observation-ledger.ts +58 -25
  169. package/scripts/observations/observations-flush-cli.ts +6 -23
  170. package/scripts/observations/observations-flush-landing.ts +2 -1
  171. package/scripts/observations/observations-flush.ts +29 -20
  172. package/scripts/package/package-scout-cli.ts +8 -16
  173. package/scripts/ports/port-command.ts +2 -1
  174. package/scripts/propagate/propagate-publish.ts +6 -7
  175. package/scripts/propagate/propagate-steps.ts +2 -1
  176. package/scripts/propagate/propagate.ts +3 -2
  177. package/scripts/refactor/import-graph.ts +2 -6
  178. package/scripts/registry-migration/migrate.ts +3 -2
  179. package/scripts/registry-migration/user-npmrc.ts +2 -2
  180. package/scripts/release/release-cli.ts +2 -1
  181. package/scripts/repo/repo-setting.ts +10 -8
  182. package/scripts/review/review-finding-ledger.ts +3 -3
  183. package/scripts/review/review-findings-cli.ts +4 -11
  184. package/scripts/review/review-record-cli.ts +14 -11
  185. package/scripts/review/review-record.ts +6 -16
  186. package/scripts/rules/decision-oracle.ts +2 -2
  187. package/scripts/rules/rule-body-guard.ts +2 -2
  188. package/scripts/rules/rule-value-cache.ts +99 -0
  189. package/scripts/rules/rule-value-cli.ts +10 -9
  190. package/scripts/rules/rule-value.ts +1 -1
  191. package/scripts/run/run-carry-args.ts +2 -6
  192. package/scripts/run/run-carry-cli.ts +5 -7
  193. package/scripts/run/run-cut-args.ts +7 -13
  194. package/scripts/run/run-ending-cli.ts +4 -7
  195. package/scripts/run/run-event-stream.ts +2 -5
  196. package/scripts/run/run-liveness-cli.ts +4 -7
  197. package/scripts/run/run-progress-args.ts +2 -6
  198. package/scripts/run/run-progress-cli.ts +2 -1
  199. package/scripts/run/run-tail-cli.ts +3 -3
  200. package/scripts/run/run-tidy-stashes.ts +7 -14
  201. package/scripts/run/run-wake-cli.ts +2 -6
  202. package/scripts/run/run-wake-driver.ts +15 -2
  203. package/scripts/run/run-wake-session.ts +14 -4
  204. package/scripts/safe-chain/project-config.ts +3 -6
  205. package/scripts/security/security-audit-provision.ts +6 -7
  206. package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
  207. package/scripts/sync/directory-copy-guard.ts +2 -1
  208. package/scripts/sync/sync-ai-files.ts +3 -3
  209. package/scripts/sync/template-source-logic.ts +3 -4
  210. package/scripts/test/e2e-retry-check.ts +3 -6
  211. package/scripts/ui/ui-routes-cli.ts +3 -6
  212. package/scripts/version/bump-version.ts +2 -1
  213. package/scripts/version/version-remote.ts +18 -27
  214. package/scripts/version/version-targets.ts +3 -3
  215. package/.claude/skills/workflow-commands/rule-residency.md +0 -267
  216. package/evals/scenarios/consult-not-execute.json +0 -20
  217. package/evals/scenarios/no-implicit-workflow.json +0 -26
  218. package/evals/scenarios/overrides-both-locations.json +0 -29
  219. package/evals/scenarios/upstream-interrupt.json +0 -23
  220. package/evals/scenarios/workflow-skill-loaded.json +0 -16
  221. package/prompts/agent-lint-protocol.json +0 -79
  222. package/prompts/agent-rules.md +0 -63
  223. package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
  224. package/prompts/collaboration-workflow/distributed-docs.md +0 -7
  225. package/prompts/collaboration-workflow/durable-rules.md +0 -24
  226. package/prompts/collaboration-workflow/latest-first.md +0 -14
  227. package/prompts/collaboration-workflow/no-clones.md +0 -11
  228. package/prompts/collaboration-workflow/simplicity-first.md +0 -9
  229. package/prompts/collaboration-workflow/single-source-rules.md +0 -13
  230. package/scripts/build/shim-shell.ts +0 -25
  231. package/scripts/claude/skill-meta.ts +0 -37
  232. package/scripts/document/document-scan.ts +0 -108
  233. package/scripts/document/generate-catalog.ts +0 -69
  234. package/scripts/document/retired-phrases.ts +0 -143
  235. package/scripts/document/review-agent-type.ts +0 -46
  236. package/scripts/document/single-source.ts +0 -73
  237. package/scripts/git/git-fixture-workspace.ts +0 -88
  238. package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
  239. package/scripts/hooks/prompt-hooks.ts +0 -38
  240. package/scripts/init/hook-command-bootstrap.ts +0 -83
  241. package/scripts/issue/issue-logic.ts +0 -50
  242. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
  243. package/scripts/josh/josh-script-reader.ts +0 -11
  244. package/scripts/package/production-dependency-graph.ts +0 -68
  245. package/scripts/retrospective/retrospective-cli.ts +0 -97
  246. package/scripts/retrospective/retrospective.ts +0 -127
  247. package/scripts/rules/delivered-rules-harness.ts +0 -72
  248. package/scripts/rules/top-level-scripts-guard.ts +0 -47
  249. package/scripts/run/run-carry-flush.ts +0 -58
  250. package/scripts/run/run-transcript.golden.txt +0 -57
  251. package/scripts/run/run-wake-work.ts +0 -108
  252. package/scripts/sync/dependabot-pin-logic.ts +0 -135
  253. package/scripts/test/classify-isolation.ts +0 -149
  254. package/scripts/test/http-fault-injection.ts +0 -79
  255. package/scripts/test/josh-harness-environment.ts +0 -260
  256. package/scripts/test/josh-harness.ts +0 -96
  257. package/scripts/test/pilot-files.ts +0 -452
  258. package/scripts/test/pilot-measure.ts +0 -78
  259. package/scripts/test/test-network-guard.ts +0 -453
  260. package/scripts/test/test-repository-guard.ts +0 -317
  261. package/scripts/test/test-state-guard.ts +0 -85
  262. package/scripts/test/test-stdout-guard.ts +0 -54
  263. package/scripts/test/test-telegram-guard.ts +0 -90
  264. package/scripts/test/unit-guard-environment.ts +0 -12
  265. package/scripts/test/unit-projects.ts +0 -139
  266. package/scripts/test/vitest-include-globs.ts +0 -12
  267. package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
  268. package/scripts-ai/completion-gate.ts +0 -13
  269. package/scripts-ai/environment-loader.ts +0 -9
  270. /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
  271. /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
  272. /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
@@ -1,6 +1,7 @@
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
+ import { error_text } from '#scripts/lib/error-message'
4
5
  import {
5
6
  run_ending,
6
7
  UNREADABLE_VERDICT,
@@ -56,11 +57,7 @@ interface ParsedArguments {
56
57
  }
57
58
 
58
59
  function read_arguments(argv: ReadonlyArray<string>): ParsedArguments | undefined {
59
- try {
60
- return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true })
61
- } catch {
62
- return undefined
63
- }
60
+ return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, allowPositionals: true })
64
61
  }
65
62
 
66
63
  function is_valid(parsed: ParsedArguments): boolean {
@@ -96,7 +93,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
96
93
  try {
97
94
  return report(await run_ending.check(request))
98
95
  } catch (error) {
99
- return report_unreadable(error instanceof Error ? error.message : String(error))
96
+ return report_unreadable(error_text.message_of(error))
100
97
  }
101
98
  }
102
99
 
@@ -1,4 +1,5 @@
1
1
  import { stamp_file } from '#scripts/josh/stamp-file'
2
+ import { json_value } from '#scripts/lib/json-value'
2
3
  import { z } from 'zod'
3
4
 
4
5
  // The run's append-only, ordered event stream — the surface a reader who was away rebuilds the run's
@@ -143,11 +144,7 @@ function is_present(event: RunEvent | undefined): event is RunEvent {
143
144
  // A malformed line reads as no event rather than throwing, so one truncated append — a write that lost a
144
145
  // race — never makes the whole stream unreadable.
145
146
  function parse_line(line: string): RunEvent | undefined {
146
- try {
147
- return event_schema.safeParse(JSON.parse(line)).data
148
- } catch {
149
- return undefined
150
- }
147
+ return event_schema.safeParse(json_value.parse_or_undefined(line)).data
151
148
  }
152
149
 
153
150
  // Every event on the stream, oldest first. An absent or unreadable file is an empty stream, the same
@@ -1,6 +1,7 @@
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
+ import { error_text } from '#scripts/lib/error-message'
4
5
  import { run_issue_number } from './run-issue-number'
5
6
  import {
6
7
  MS_PER_MINUTE,
@@ -89,11 +90,7 @@ interface ParsedArguments {
89
90
  }
90
91
 
91
92
  function read_arguments(argv: ReadonlyArray<string>): ParsedArguments | undefined {
92
- try {
93
- return parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true })
94
- } catch {
95
- return undefined
96
- }
93
+ return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, allowPositionals: true })
97
94
  }
98
95
 
99
96
  function is_valid_target(parsed: ParsedArguments): boolean {
@@ -154,7 +151,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
154
151
  try {
155
152
  return report(await run_liveness.check(request))
156
153
  } catch (error) {
157
- return report_undetermined(error instanceof Error ? error.message : String(error))
154
+ return report_undetermined(error_text.message_of(error))
158
155
  }
159
156
  }
160
157
 
@@ -1,4 +1,4 @@
1
- import { parseArgs } from 'node:util'
1
+ import { cli_flags } from '#scripts/lib/cli-flags'
2
2
  import { run_progress_config } from './run-progress-config'
3
3
 
4
4
  // The argument half of `josh run:progress`, kept apart from the watcher so the CLI stays inside the file
@@ -37,11 +37,7 @@ interface ParsedValues {
37
37
  }
38
38
 
39
39
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
40
- try {
41
- return parseArgs({ args: [...argv], options: OPTIONS }).values
42
- } catch {
43
- return undefined
44
- }
40
+ return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS })?.values
45
41
  }
46
42
 
47
43
  // A hand-typed `--interval` outranks the environment, the environment outranks the interval the
@@ -5,6 +5,7 @@ import { backlog_arrival, type ArrivalProbe } from '#scripts/backlog/backlog-arr
5
5
  import { backlog_ready } from '#scripts/backlog/backlog-ready'
6
6
  import { gh_spawn } from '#scripts/gh/gh-spawn'
7
7
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
8
+ import { error_text } from '#scripts/lib/error-message'
8
9
  import { run_event_stream_emit } from './run-event-stream-emit'
9
10
  import { run_progress, type ProgressState } from './run-progress'
10
11
  import { run_progress_args, type ParsedValues } from './run-progress-args'
@@ -229,7 +230,7 @@ function decline(loop: WatchLoop, now_ms: number, notice: string): WatchLoop {
229
230
  // under load — the very load this command exists to report — would otherwise take the reporting down
230
231
  // silently for the rest of an unattended run, with the parent not waiting on it and nothing saying why.
231
232
  function to_notice(error: unknown): string {
232
- return `${FAILED_TICK_PREFIX} ${error instanceof Error ? error.message : String(error)}`
233
+ return `${FAILED_TICK_PREFIX} ${error_text.message_of(error)}`
233
234
  }
234
235
 
235
236
  async function attempt(
@@ -17,9 +17,9 @@ import { run_tail, type TailSection } from './run-tail'
17
17
  //
18
18
  // **A dispatched lane child skips the ledger step** (joshuafolkken/kit#2492). A flush opens a
19
19
  // ledger-only pull request and waits out its whole CI before it merges — minutes per issue, spent on a
20
- // file only the retrospective and the measurements read. The line stays appended in the primary
21
- // checkout, and the `backlogrun` parent flushes once at its end (`run:carry --end`, `run-carry-flush.ts`).
22
- // A single run outside a lane keeps the step: its one end is already the run's end.
20
+ // file only the retrospective and the measurements read — and a lane has nothing for it: its lines
21
+ // merged with its own pull request (joshuafolkken/kit#2919). A single run outside a lane keeps the
22
+ // step for a line written on the default branch outside any issue's run.
23
23
 
24
24
  const ARGV_OFFSET = 2
25
25
  const SUCCESS_EXIT_CODE = 0
@@ -1,4 +1,3 @@
1
- import { readFile } from 'node:fs/promises'
2
1
  import { git_stash, type StashEntry } from '#scripts/git/git-stash'
3
2
  import { stash_orphans } from '#scripts/git/stash-orphans'
4
3
  import { stash_sweep_lock } from '#scripts/git/stash-sweep-lock'
@@ -18,16 +17,8 @@ const READ_CONCURRENCY = 8
18
17
  const GONE_REASON = 'already dropped by another run'
19
18
  const BUSY_REASON = 'another run held the stash sweep lock'
20
19
 
21
- async function read_ledger(ledger_path: string): Promise<string> {
22
- try {
23
- return await readFile(ledger_path, 'utf8')
24
- } catch {
25
- return ''
26
- }
27
- }
28
-
29
- // An entry cut before joshuafolkken/kit#2724 holds its lines at the ledger's old path, so the lines of
30
- // every ledger path it touches are carried to the current ledger.
20
+ // An entry cut before joshuafolkken/kit#2919 holds its lines at an old single-file path, so the lines
21
+ // of every ledger path it touches are carried to the current ledger.
31
22
  async function added_ledger_lines(
32
23
  hash: string,
33
24
  ledger_paths: ReadonlyArray<string>,
@@ -43,16 +34,18 @@ async function added_ledger_lines(
43
34
  return added
44
35
  }
45
36
 
46
- // How many ledger lines went into the primary checkout's ledger before the drop.
37
+ // How many ledger lines went into the running work tree's ledger before the drop. The duplicate check
38
+ // reads every issue's file, and the lines land in the file this tree writes (joshuafolkken/kit#2919),
39
+ // so the run's own commit takes them to the default branch.
47
40
  async function carry_ledger(hash: string): Promise<number> {
48
41
  const changed = await git_stash.changed_paths(hash)
49
42
  const ledger_paths = changed.filter((file_path) => observation_ledger.is_ledger_path(file_path))
50
43
 
51
44
  if (ledger_paths.length === 0) return 0
52
45
 
53
- const ledger_path = observation_ledger_home.ledger_path()
46
+ const ledger_path = await observation_ledger_home.writer_path(new Date())
54
47
  const added = await added_ledger_lines(hash, ledger_paths)
55
- const carried = run_tidy.ledger_carry(added, await read_ledger(ledger_path))
48
+ const carried = run_tidy.ledger_carry(added, (await observation_ledger_home.read()) ?? '')
56
49
 
57
50
  await observation_ledger_home.append(ledger_path, carried)
58
51
 
@@ -2,9 +2,9 @@
2
2
  import { randomUUID } from 'node:crypto'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
- import { parseArgs } from 'node:util'
6
5
  import type { AgentProfile } from '#scripts/agent/agent-role-profile'
7
6
  import { telegram_notify } from '#scripts/git/telegram-notify'
7
+ import { cli_flags } from '#scripts/lib/cli-flags'
8
8
  import { run_carry, type CarryRead } from './run-carry'
9
9
  import { run_event_stream } from './run-event-stream'
10
10
  import { run_headless } from './run-headless'
@@ -120,11 +120,7 @@ function note_to_stderr(note: string): void {
120
120
  }
121
121
 
122
122
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
123
- try {
124
- return parseArgs({ args: [...argv], options: OPTIONS, strict: true }).values
125
- } catch {
126
- return undefined
127
- }
123
+ return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS, strict: true })?.values
128
124
  }
129
125
 
130
126
  function text_of(value: string | boolean | undefined): string | undefined {
@@ -6,6 +6,8 @@ import type { DriveResult } from './run-wake-loop'
6
6
  const SUCCESS = 0
7
7
  const FIRST_LINE = 0
8
8
  const RESUME_PREFIX = 'resume: '
9
+ const DETAILS_TAIL_LENGTH = 2048
10
+ const TRUNCATION_MARK = '... '
9
11
 
10
12
  function driver_args(invocation: string): ReadonlyArray<string> | undefined {
11
13
  const rebuilt = run_invocation.rebuild(invocation)
@@ -24,13 +26,24 @@ function is_final_stop(verdict: string | undefined, read: CarryRead): boolean {
24
26
  return verdict?.startsWith('stop') === true && read.kind === 'none'
25
27
  }
26
28
 
29
+ // **The details are the driver's whole stderr, so only their tail is handed on, and it goes last**
30
+ // (joshuafolkken/kit#2931). The stderr carries every command the driver ran since it started, which
31
+ // after a long run is tens of kilobytes — far past what `is_safe_value` lets into an argv element. Its
32
+ // tail is where the reason for stopping is; placing it last means the cap `wake_argv` applies to the
33
+ // whole prompt can only ever cut into the details, never the verdict, the resume line or the epic note.
34
+ function details_tail(details: string): string {
35
+ return details.length > DETAILS_TAIL_LENGTH
36
+ ? `${TRUNCATION_MARK}${details.slice(-DETAILS_TAIL_LENGTH)}`
37
+ : details
38
+ }
39
+
27
40
  function handoff_material(verdict: string, resume: string, details: string): string {
28
- const context = details === '' ? '' : `\nDetails: ${details}`
41
+ const context = details === '' ? '' : `\nDetails: ${details_tail(details)}`
29
42
  const epic = verdict.startsWith('epic #')
30
43
  ? '\nThe named item is an epic. Follow backlogrun-steps.md named epic procedure; do not launch the epic root as a fullrun child. After all children merge or park, record the root with pnpm josh run:carry --done <epic-number> --owner "$PPID" before continuing to the next named item.'
31
44
  : ''
32
45
 
33
- return `Driver result: ${verdict}\n${resume}${context}${epic}`
46
+ return `Driver result: ${verdict}\n${resume}${epic}${context}`
34
47
  }
35
48
 
36
49
  function driver_result(out: string, read: CarryRead, details = ''): DriveResult {
@@ -37,6 +37,8 @@ const WAKE_FLAGS = claude_agent_argv.AGENT_FLAGS
37
37
  const LOOP_FLAG = '--loop'
38
38
  const INTERVAL_FLAG = '--interval'
39
39
  const SUPERVISOR_SESSION = 'run-wake-supervisor'
40
+ // eslint-disable-next-line no-control-regex -- matching control characters is the point
41
+ const CONTROL_CHARACTERS = /[\u{0}-\u{1F}\u{7F}]/gu
40
42
 
41
43
  function scheduler_profile(): AgentProfile | undefined {
42
44
  const resolved = agent_role_profile.resolve(agent_role_profile.SCHEDULER)
@@ -96,6 +98,17 @@ function resolved_argv(invocation: string, cwd?: string): AgentArgvResult {
96
98
  : agent_argv.resolve_in(invocation, agent_role_profile.SCHEDULER, cwd)
97
99
  }
98
100
 
101
+ // **Handoff material is made safe to launch rather than refused** (joshuafolkken/kit#2931). It is the
102
+ // driver's own output, not the record, so it is flattened — a newline becomes a separator, any other
103
+ // control character a space — and the prompt is cut at the cap `is_safe_value` enforces. The driver puts
104
+ // its verdict and resume line first, so the cut only ever shortens the details. The check itself is
105
+ // untouched: the argv still passes `is_safe_argv` before anything is spawned.
106
+ function handoff_prompt(matched: string, material: string): string {
107
+ const flattened = material.replaceAll('\n', ' | ').replaceAll(CONTROL_CHARACTERS, ' ')
108
+
109
+ return `${matched} | Driver handoff: ${flattened}`.slice(0, agent_role_profile.MAX_VALUE_LENGTH)
110
+ }
111
+
99
112
  function wake_argv(
100
113
  invocation: string,
101
114
  profile?: AgentProfile,
@@ -104,10 +117,7 @@ function wake_argv(
104
117
  ): AgentArgvResult | undefined {
105
118
  const matched = safe_invocation(invocation)
106
119
  if (matched === undefined) return undefined
107
- const prompt =
108
- typeof session === 'object'
109
- ? `${matched} | Driver handoff: ${session.material.replaceAll('\n', ' | ')}`
110
- : matched
120
+ const prompt = typeof session === 'object' ? handoff_prompt(matched, session.material) : matched
111
121
  const session_id = typeof session === 'object' ? session.id : session
112
122
 
113
123
  return profile === undefined
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
+ import { file_reader } from '#scripts/lib/read-file'
3
4
  import { release_age } from '#scripts/version/release-age'
4
5
  import { yaml_document } from '#scripts/yaml/yaml-document'
5
6
  import { dump } from 'js-yaml'
@@ -126,17 +127,13 @@ function merge_project_config(existing: string, workspace: string, npmrc = ''):
126
127
  return replace_safe_chain_block(existing, merged)
127
128
  }
128
129
 
129
- function read_optional(file_path: string): string {
130
- return existsSync(file_path) ? readFileSync(file_path, 'utf8') : ''
131
- }
132
-
133
130
  function sync_project_config(root: string): boolean {
134
131
  const workspace_path = path.join(root, WORKSPACE_FILE)
135
132
  if (!existsSync(workspace_path)) return false
136
133
  const config_path = path.join(root, PROJECT_CONFIG_FILE)
137
- const existing = read_optional(config_path)
134
+ const existing = file_reader.read_file_or_empty(config_path)
138
135
  const workspace = readFileSync(workspace_path, 'utf8')
139
- const npmrc = read_optional(path.join(root, NPMRC_FILE))
136
+ const npmrc = file_reader.read_file_or_empty(path.join(root, NPMRC_FILE))
140
137
  const merged = merge_project_config(existing, workspace, npmrc)
141
138
  if (merged === existing) return false
142
139
  writeFileSync(config_path, merged)
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
6
6
  import { stamp_file } from '#scripts/josh/stamp-file'
7
+ import { error_text } from '#scripts/lib/error-message'
7
8
  import { security_audit } from './security-audit'
8
9
  import { security_audit_logic } from './security-audit-logic'
9
10
  import { security_audit_provision_logic, type ScannerAsset } from './security-audit-provision-logic'
@@ -14,10 +15,6 @@ const FORCE_FLAG = '--force'
14
15
  const PATH_LOCATION = 'on PATH'
15
16
  const { RETRY_INTERVAL_MS } = security_audit_provision_logic
16
17
 
17
- function describe_error(error: unknown): string {
18
- return error instanceof Error ? error.message : String(error)
19
- }
20
-
21
18
  // `content` absent is the failure, and `reason` says which one. A 404 — what a bumped version whose
22
19
  // asset was renamed looks like, and the one failure that never fixes itself — would otherwise read
23
20
  // exactly like a transient 503, and a rejected request like neither.
@@ -41,7 +38,7 @@ async function download(url: string, timeout_ms: number): Promise<DownloadOutcom
41
38
 
42
39
  return { content: Buffer.from(await response.arrayBuffer()), reason: '' }
43
40
  } catch (error) {
44
- return { reason: describe_error(error) }
41
+ return { reason: error_text.message_of(error) }
45
42
  }
46
43
  }
47
44
 
@@ -128,7 +125,7 @@ async function attempt(
128
125
  } catch (error) {
129
126
  return record_failure(
130
127
  target_path,
131
- security_audit_provision_logic.format_provision_error(describe_error(error)),
128
+ security_audit_provision_logic.format_provision_error(error_text.message_of(error)),
132
129
  )
133
130
  }
134
131
  }
@@ -175,7 +172,9 @@ async function main(): Promise<void> {
175
172
  try {
176
173
  console.info(await report(PROJECT_ROOT, process.platform, process.arch, is_forced))
177
174
  } catch (error) {
178
- console.warn(security_audit_provision_logic.format_provision_error(describe_error(error)))
175
+ console.warn(
176
+ security_audit_provision_logic.format_provision_error(error_text.message_of(error)),
177
+ )
179
178
  }
180
179
  }
181
180
 
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { git_command } from '#scripts/git/git-command'
6
+ import { error_text } from '#scripts/lib/error-message'
6
7
  import { managed_config_scope } from '#scripts/sync/managed-config-scope'
7
8
  import { z } from 'zod'
8
9
  import {
@@ -69,7 +70,7 @@ async function fetch_hotspots(url: string): Promise<HotspotFetch> {
69
70
 
70
71
  return { hotspots: parsed.hotspots ?? [] }
71
72
  } catch (error) {
72
- return { error: error instanceof Error ? error.message : String(error) }
73
+ return { error: error_text.message_of(error) }
73
74
  }
74
75
  }
75
76
 
@@ -1,6 +1,7 @@
1
1
  import { cpSync, lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { transform_copied_content } from '#scripts/init/init-copy-content'
4
+ import { error_text } from '#scripts/lib/error-message'
4
5
  import { file_content } from './file-content'
5
6
 
6
7
  // `josh init` and `josh sync` both copy a distributed directory with `cpSync`, and both have to
@@ -143,7 +144,7 @@ function copy_directory(source_path: string, destination_path: string): boolean
143
144
  // throw here would end `josh init` or `josh sync` in the middle of their work. The message is
144
145
  // returned for the caller to report, the same way a blocker is.
145
146
  function failure_message(prefix: string, error: unknown): string {
146
- return `${prefix} (${error instanceof Error ? error.message : String(error)})`
147
+ return `${prefix} (${error_text.message_of(error)})`
147
148
  }
148
149
 
149
150
  // The two failures are reported apart because they leave the destination in opposite states, and
@@ -5,6 +5,7 @@ import { transform_copied_content } from '#scripts/init/init-copy-content'
5
5
  import { init_logic } from '#scripts/init/init-logic'
6
6
  import { PACKAGE_DIR, PROJECT_ROOT } from '#scripts/init/init-paths'
7
7
  import type { ProjectShape } from '#scripts/init/project-profile'
8
+ import { file_reader } from '#scripts/lib/read-file'
8
9
  import { copy_directory_failure, directory_copy_blocker } from './directory-copy-guard'
9
10
  import { file_content } from './file-content'
10
11
  import { REMOVED_SKILL_MANIFEST } from './removed-skill-manifest'
@@ -70,8 +71,7 @@ function sync_workspace_yaml(
70
71
  is_force = false,
71
72
  ): boolean {
72
73
  const template = readFileSync(template_path, 'utf8')
73
- const existing =
74
- !is_force && existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : ''
74
+ const existing = is_force ? '' : file_reader.read_file_or_empty(destination_path)
75
75
  const merged = init_logic.merge_workspace_yaml(existing, template)
76
76
 
77
77
  return file_content.write_text_if_changed(destination_path, merged)
@@ -155,7 +155,7 @@ function ensured_claude_md(existing: string | undefined, shape?: ProjectShape):
155
155
  // without ever disturbing those additions — so this ignores --force, which would otherwise mean
156
156
  // discarding a consumer's content.
157
157
  function sync_claude_md(destination_path: string, shape?: ProjectShape): void {
158
- const existing = existsSync(destination_path) ? readFileSync(destination_path, 'utf8') : undefined
158
+ const existing = file_reader.read_optional(destination_path)
159
159
  const ensured = ensured_claude_md(existing, shape)
160
160
 
161
161
  if (ensured === existing) {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { copyFileSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import { package_path } from '#scripts/init/init-paths'
4
+ import { file_reader } from '#scripts/lib/read-file'
4
5
  import { z } from 'zod'
5
6
 
6
7
  interface TemplateSourcePair {
@@ -39,9 +40,7 @@ function read_file(relative_path: string): string {
39
40
  }
40
41
 
41
42
  function read_optional_file(relative_path: string): string {
42
- const full_path = package_path(relative_path)
43
-
44
- return existsSync(full_path) ? readFileSync(full_path, 'utf8') : ''
43
+ return file_reader.read_file_or_empty(package_path(relative_path))
45
44
  }
46
45
 
47
46
  function hash_text(text: string): string {
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env tsx
2
- import { appendFileSync, readdirSync, readFileSync, statSync } from 'node:fs'
2
+ import { appendFileSync, readdirSync, statSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { file_reader } from '#scripts/lib/read-file'
5
6
 
6
7
  // Whether the preview server process died during a failed E2E attempt.
7
8
  //
@@ -59,11 +60,7 @@ function has_crash_signature(log_text: string): boolean {
59
60
  }
60
61
 
61
62
  function read_file_safe(file_path: string): string {
62
- try {
63
- return readFileSync(file_path, 'utf8')
64
- } catch {
65
- return ''
66
- }
63
+ return file_reader.read_if_readable(file_path) ?? ''
67
64
  }
68
65
 
69
66
  // Every file under the directory, because wrangler names the debug log after the run and a job that
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env tsx
2
- import { readdirSync, readFileSync } from 'node:fs'
2
+ import { readdirSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { changed_paths } from '#scripts/git/changed-paths'
6
6
  import { git_command } from '#scripts/git/git-command'
7
+ import { file_reader } from '#scripts/lib/read-file'
7
8
  import { ui_routes } from './ui-routes'
8
9
 
9
10
  // `josh ui:routes [--staged]` — list the screenshot-target routes the current change touches
@@ -59,11 +60,7 @@ function route_files(root: string): Array<RouteFile> {
59
60
  }
60
61
 
61
62
  function read_text(file: string): string {
62
- try {
63
- return readFileSync(file, 'utf8')
64
- } catch {
65
- return ''
66
- }
63
+ return file_reader.read_if_readable(file) ?? ''
67
64
  }
68
65
 
69
66
  // A route file imports the component when its source names the component's file — the import
@@ -2,6 +2,7 @@
2
2
  import { readFileSync, writeFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { error_text } from '#scripts/lib/error-message'
5
6
  import { json_object_schema, package_with_version_schema } from '#scripts/lib/schemas'
6
7
  import semver from 'semver'
7
8
 
@@ -61,7 +62,7 @@ function main(): void {
61
62
  try {
62
63
  bump_version(bump_type as BumpType)
63
64
  } catch (error) {
64
- console.error(error instanceof Error ? error.message : String(error))
65
+ console.error(error_text.message_of(error))
65
66
  process.exit(1)
66
67
  }
67
68
  }
@@ -1,9 +1,8 @@
1
1
  import { git_gh_exec } from '#scripts/git/git-gh-exec'
2
- import { execaSync } from 'execa'
2
+ import { error_text } from '#scripts/lib/error-message'
3
3
  import { safe_json_parse } from './parse-json'
4
4
  import { release_age } from './release-age'
5
5
 
6
- const GH_FAILURE_FALLBACK = 'gh api failed'
7
6
  // Enough history to reach past a quarantine window into the newest release that has aged out of it.
8
7
  // The endpoint's default page is a single entry, so it is widened to the API's maximum rather than
9
8
  // paged through: a page that cannot span the window resolves no aged release at all, which silently
@@ -23,32 +22,27 @@ function require_endpoint(versions_endpoint: string | undefined, package_name: s
23
22
  )
24
23
  }
25
24
 
26
- // Turn a failed `gh api` result into a concise detail line: prefer gh's own stderr
27
- // (e.g. "gh: Not Found (HTTP 404)"), falling back to execa's short message.
28
- function describe_failure(result: { stderr: string; shortMessage?: string | undefined }): string {
29
- const stderr = result.stderr.trim()
30
-
31
- if (stderr !== '') return stderr
32
-
33
- return result.shortMessage ?? GH_FAILURE_FALLBACK
34
- }
35
-
36
25
  // Fetch the latest published version from a GitHub Packages versions endpoint. The endpoint is
37
26
  // supplied per package (e.g. `/users/joshuafolkken/packages/npm/kit/versions?per_page=1`) so the
38
27
  // same fetcher serves kit, jgame, and app-kit. Guards an undefined/empty endpoint and wraps
39
- // `gh api` failures with an actionable message instead of a raw ExecaSyncError stack.
28
+ // `gh api` failures with an actionable message instead of a raw ExecaSyncError stack. The detail is
29
+ // the shared layer's failure text: gh's own stderr (e.g. "gh: Not Found (HTTP 404)") when it wrote
30
+ // one, execa's message otherwise, followed by any JSON error body gh wrote to stdout.
40
31
  function fetch_latest_version(versions_endpoint: string | undefined, package_name: string): string {
41
32
  const endpoint = require_endpoint(versions_endpoint, package_name)
42
- const result = execaSync('gh', ['api', endpoint, '--jq', '.[0].name'], {
43
- ...git_gh_exec.direct_environment(),
44
- reject: false,
45
- })
46
33
 
47
- if (result.exitCode === 0) return result.stdout.trim()
34
+ try {
35
+ return git_gh_exec.exec_gh_api_sync({ path: endpoint, jq_filter: '.[0].name' }).trim()
36
+ } catch (error) {
37
+ const detail = error_text.message_of(error)
48
38
 
49
- throw new Error(
50
- `Failed to fetch latest version for ${package_name} via ${endpoint}: ${describe_failure(result)}`,
51
- )
39
+ throw new Error(
40
+ `Failed to fetch latest version for ${package_name} via ${endpoint}: ${detail}`,
41
+ {
42
+ cause: error,
43
+ },
44
+ )
45
+ }
52
46
  }
53
47
 
54
48
  // Rewrite the endpoint's page size. The endpoint is consumer-overridable, so `per_page` is set
@@ -73,12 +67,9 @@ function fetch_release_times(
73
67
  ): Record<string, string> | undefined {
74
68
  if (versions_endpoint === undefined || versions_endpoint.trim() === '') return undefined
75
69
  const endpoint = with_page_size(versions_endpoint, TIMES_PAGE_SIZE)
76
- const result = execaSync('gh', ['api', endpoint, '--jq', TIMES_JQ], {
77
- ...git_gh_exec.direct_environment(),
78
- reject: false,
79
- })
80
- if (result.exitCode !== 0) return undefined
81
- const parsed = release_age.release_times_schema.safeParse(safe_json_parse(result.stdout))
70
+ const stdout = git_gh_exec.read_gh_api_sync({ path: endpoint, jq_filter: TIMES_JQ })
71
+ if (stdout === undefined) return undefined
72
+ const parsed = release_age.release_times_schema.safeParse(safe_json_parse(stdout))
82
73
 
83
74
  return parsed.success ? parsed.data : undefined
84
75
  }
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
1
  import path from 'node:path'
2
+ import { file_reader } from '#scripts/lib/read-file'
3
3
  import { package_version_schema, pnpm_ls_global_schema } from '#scripts/lib/schemas'
4
4
  import { execaSync } from 'execa'
5
5
  import { safe_json_parse } from './parse-json'
@@ -42,7 +42,7 @@ function project_package_path(cwd: string, package_name: string): string {
42
42
 
43
43
  function read_project_version(cwd: string, package_name: string): string | undefined {
44
44
  const package_path = project_package_path(cwd, package_name)
45
- const raw = existsSync(package_path) ? readFileSync(package_path, 'utf8') : undefined
45
+ const raw = file_reader.read_optional(package_path)
46
46
 
47
47
  return parse_project_version(raw)
48
48
  }
@@ -52,7 +52,7 @@ function read_project_version(cwd: string, package_name: string): string | undef
52
52
  // package version under node_modules.
53
53
  function read_workspace_version(cwd: string): string | undefined {
54
54
  const package_path = path.join(cwd, PACKAGE_JSON)
55
- const raw = existsSync(package_path) ? readFileSync(package_path, 'utf8') : undefined
55
+ const raw = file_reader.read_optional(package_path)
56
56
 
57
57
  return parse_project_version(raw)
58
58
  }