@joshuafolkken/kit 1.1005.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 -58
  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 +32 -6
  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 +23 -14
  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
3
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
4
+ import { error_text } from '#scripts/lib/error-message'
4
5
  import { git_command } from './git-command'
5
6
  import { gone_branch, type PruneResult } from './gone-branch'
6
7
 
@@ -65,7 +66,7 @@ async function prune_quietly(default_branch: string): Promise<void> {
65
66
  try {
66
67
  report_prune(await gone_branch.prune(default_branch))
67
68
  } catch (error) {
68
- console.error(`branch prune skipped: ${error instanceof Error ? error.message : String(error)}`)
69
+ console.error(`branch prune skipped: ${error_text.message_of(error)}`)
69
70
  }
70
71
  }
71
72
 
@@ -104,7 +105,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
104
105
  try {
105
106
  return await dispatch(argv)
106
107
  } catch (error) {
107
- console.error(error instanceof Error ? error.message : String(error))
108
+ console.error(error_text.message_of(error))
108
109
 
109
110
  return FAILURE_EXIT_CODE
110
111
  }
@@ -111,7 +111,7 @@ const rest_comment_schema = z.looseObject({
111
111
  // Labels come back as objects, so the name is picked out here rather than at every call site.
112
112
  const issue_label_schema = z.object({ name: z.string() })
113
113
 
114
- // `state` is optional because most readers ignore it — only `git-epic-add.ts` consults it, to refuse
114
+ // `state` is optional because most readers ignore it — only `epic-add.ts` consults it, to refuse
115
115
  // adding a child to a closed epic (joshuafolkken/kit#2337). A reader that never asks the state field
116
116
  // still parses cleanly.
117
117
  const epic_subject_schema = z.object({
@@ -1,4 +1,4 @@
1
- import { parseArgs } from 'node:util'
1
+ import { cli_flags } from '#scripts/lib/cli-flags'
2
2
 
3
3
  // `josh stash:pop` turns its `argv` into a single request: the message of the stash to pop. Parsing
4
4
  // lives here, acting on the stack lives in `stash-pop-cli.ts`, and a malformed command line is
@@ -21,18 +21,12 @@ interface Request {
21
21
  }
22
22
 
23
23
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
24
- try {
25
- const parsed = parseArgs({
26
- args: [...argv],
27
- options: OPTIONS,
28
- strict: true,
29
- allowPositionals: true,
30
- })
31
-
32
- return { values: parsed.values, positionals: parsed.positionals }
33
- } catch {
34
- return undefined
35
- }
24
+ return cli_flags.parse_or_undefined({
25
+ args: [...argv],
26
+ options: OPTIONS,
27
+ strict: true,
28
+ allowPositionals: true,
29
+ })
36
30
  }
37
31
 
38
32
  // The message is the sole positional and must be a non-empty string: an empty message matches every
@@ -1,4 +1,5 @@
1
1
  import { fileURLToPath } from 'node:url'
2
+ import { error_text } from '#scripts/lib/error-message'
2
3
  import { git_stash } from './git-stash'
3
4
  import { stash_pop_args, type Request } from './stash-pop-args'
4
5
 
@@ -66,7 +67,7 @@ async function answer(request: Request): Promise<number> {
66
67
  }
67
68
 
68
69
  function report_unknown(error: unknown): number {
69
- console.error(error instanceof Error ? error.message : String(error))
70
+ console.error(error_text.message_of(error))
70
71
 
71
72
  return FAILURE_EXIT_CODE
72
73
  }
@@ -1,5 +1,5 @@
1
- import type { TelegramSendInput, TelegramTaskType } from '../scripts/git/telegram-notify'
2
- import { cli_body } from '../scripts/josh/cli-body'
1
+ import { cli_body } from '#scripts/josh/cli-body'
2
+ import type { TelegramSendInput, TelegramTaskType } from './telegram-notify'
3
3
 
4
4
  /* eslint-disable @typescript-eslint/naming-convention */
5
5
  interface CliValues {
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { parseArgs } from 'node:util'
4
- import { git_error } from '../scripts/git/git-error'
5
- import { git_gh_issue_read } from '../scripts/git/git-gh-issue-read'
6
- import { git_gh_repo } from '../scripts/git/git-gh-repo'
7
- import { github_issue_url, type IssueUrlTarget } from '../scripts/git/github-issue-url'
8
- import { telegram_notify } from '../scripts/git/telegram-notify'
9
- import { load_optional_environment } from './environment-loader'
4
+ import { josh_environment_file } from '#scripts/josh/josh-environment-file'
5
+ import { git_error } from './git-error'
6
+ import { git_gh_issue_read } from './git-gh-issue-read'
7
+ import { git_gh_repo } from './git-gh-repo'
8
+ import { github_issue_url, type IssueUrlTarget } from './github-issue-url'
9
+ import { telegram_notify } from './telegram-notify'
10
10
  import { telegram_test_logic, type CliValues, type ResolvedContext } from './telegram-test-logic'
11
11
 
12
12
  const REPO_NAME_SEPARATOR = '/'
@@ -97,7 +97,7 @@ async function resolve_context(values: CliValues): Promise<ResolvedContext> {
97
97
  }
98
98
 
99
99
  async function main(): Promise<void> {
100
- load_optional_environment()
100
+ josh_environment_file.load_environment_file()
101
101
  const values = parse_cli_arguments()
102
102
  const context = await resolve_context(values)
103
103
  const input = telegram_test_logic.build_input({ values, context })
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs'
1
+ import { file_reader } from '#scripts/lib/read-file'
2
2
 
3
3
  // The rewrite half of the edit hook (joshuafolkken/kit#2314). The format hook runs prettier and
4
4
  // `eslint --fix` in place after every edit, so a file the model just wrote can be rewritten out from
@@ -85,13 +85,7 @@ function build(before: string, after: string): string | undefined {
85
85
  // handed nothing for, or one gone by the time it reads, compares as an empty string rather than
86
86
  // throwing — the hook runs after the write already succeeded and must never turn it into a failure.
87
87
  function read_text(file_path: string | undefined): string {
88
- if (file_path === undefined) return ''
89
-
90
- try {
91
- return readFileSync(file_path, 'utf8')
92
- } catch {
93
- return ''
94
- }
88
+ return file_path === undefined ? '' : (file_reader.read_if_readable(file_path) ?? '')
95
89
  }
96
90
 
97
91
  const rewrite_notice = { build, read_text }
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { is_workflow_destination } from '#scripts/claude/workflow-destination'
4
4
  import { gh_spawn } from '#scripts/gh/gh-spawn'
5
+ import { file_reader } from '#scripts/lib/read-file'
5
6
  import { managed_marker_logic } from '#scripts/managed-marker/managed-marker-logic'
6
7
  import {
7
8
  classify_path,
@@ -101,20 +102,32 @@ function did_skip_copy_if_absent(
101
102
  return false
102
103
  }
103
104
 
104
- function did_skip_workspace_yaml_copy(source_path: string, destination_path: string): boolean {
105
- if (!existsSync(destination_path)) {
106
- copy_ai_file(source_path, destination_path)
107
- console.info(` ✔ created ${WORKSPACE_YAML}`)
108
-
109
- return false
110
- }
111
-
105
+ // The merged file is written back, and reported as `updated`, only when the merge changed it — a
106
+ // second `josh init` over an already-merged file reports `unchanged` (joshuafolkken/kit#2873).
107
+ function merge_existing_workspace_yaml(source_path: string, destination_path: string): void {
112
108
  const template = readFileSync(source_path, 'utf8')
113
109
  const existing = readFileSync(destination_path, 'utf8')
114
110
  const merged = init_logic.merge_workspace_yaml(existing, template)
115
111
 
116
- if (merged !== existing) writeFileSync(destination_path, merged)
112
+ if (merged === existing) {
113
+ console.info(` ✔ unchanged ${WORKSPACE_YAML}`)
114
+
115
+ return
116
+ }
117
+
118
+ writeFileSync(destination_path, merged)
117
119
  console.info(` ✔ updated ${WORKSPACE_YAML}`)
120
+ }
121
+
122
+ function did_skip_workspace_yaml_copy(source_path: string, destination_path: string): boolean {
123
+ if (existsSync(destination_path)) {
124
+ merge_existing_workspace_yaml(source_path, destination_path)
125
+
126
+ return false
127
+ }
128
+
129
+ copy_ai_file(source_path, destination_path)
130
+ console.info(` ✔ created ${WORKSPACE_YAML}`)
118
131
 
119
132
  return false
120
133
  }
@@ -226,11 +239,7 @@ function did_skip_ai_directory_copy(directory_name: string): boolean {
226
239
  // moved onto the basic path — only that kit-written line, never the consumer's additions.
227
240
  // An unreadable file is left as it is and reported as skipped, as it was before the migration.
228
241
  function read_if_readable(destination_path: string): string | undefined {
229
- try {
230
- return readFileSync(destination_path, 'utf8')
231
- } catch {
232
- return undefined
233
- }
242
+ return file_reader.read_if_readable(destination_path)
234
243
  }
235
244
 
236
245
  function report_existing_claude_md(destination_path: string): void {
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { resolve_local_bin } from '#scripts/build/local-bin'
6
6
  import { doctor_io } from '#scripts/doctor/doctor-io'
7
+ import { error_text } from '#scripts/lib/error-message'
7
8
  import { package_version_schema, with_package_manager_schema } from '#scripts/lib/schemas'
8
9
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
9
10
  import { project_config } from '#scripts/safe-chain/project-config'
@@ -45,8 +46,11 @@ function show_sample(action: FileAction): void {
45
46
  console.info(action.create().replaceAll(/^/gmu, () => SAMPLE_INDENT))
46
47
  }
47
48
 
48
- function report_unchanged(action: FileAction): void {
49
- if (action.should_show_sample_when_unchanged === true) show_sample(action)
49
+ // A file that already holds the sample needs nothing added, so only one that differs gets it shown.
50
+ function report_unchanged(action: FileAction, existing: string): void {
51
+ const is_sample_missing = existing !== action.create()
52
+
53
+ if (is_sample_missing && action.should_show_sample_when_unchanged === true) show_sample(action)
50
54
  else console.info(` ✔ unchanged ${action.dest}`)
51
55
  }
52
56
 
@@ -59,7 +63,7 @@ function merge_existing_file(
59
63
  const merged = merge_function(existing)
60
64
 
61
65
  if (merged === existing) {
62
- report_unchanged(action)
66
+ report_unchanged(action, existing)
63
67
 
64
68
  return
65
69
  }
@@ -375,7 +379,7 @@ function run_cli(args: ReadonlyArray<string>): void {
375
379
  try {
376
380
  if (main(args)) print_start_hint()
377
381
  } catch (error) {
378
- console.error(`\n✖ ${error instanceof Error ? error.message : String(error)}\n`)
382
+ console.error(`\n✖ ${error_text.message_of(error)}\n`)
379
383
  process.exitCode = 1
380
384
  }
381
385
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path'
2
+ import { error_text } from '#scripts/lib/error-message'
2
3
  import { repository_labels } from '#scripts/repo/repository-labels'
3
4
  import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
4
5
  import { main as init_main } from './init'
@@ -119,7 +120,7 @@ async function run_step(
119
120
  try {
120
121
  await STEP_ACTIONS[step](context)
121
122
  } catch (error) {
122
- const cause = error instanceof Error ? error.message : String(error)
123
+ const cause = error_text.message_of(error)
123
124
 
124
125
  throw new Error(start_plan.progress_report(step, completed, cause), { cause: error })
125
126
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
+ import { error_text } from '#scripts/lib/error-message'
3
4
  import { project_profile, type ProjectProfile } from './project-profile'
4
5
  import {
5
6
  start_plan,
@@ -96,7 +97,7 @@ async function run(args: ReadonlyArray<string>, is_tty: boolean): Promise<number
96
97
 
97
98
  return 0
98
99
  } catch (error) {
99
- console.error(error instanceof Error ? error.message : String(error))
100
+ console.error(error_text.message_of(error))
100
101
 
101
102
  return 1
102
103
  }
@@ -2,6 +2,7 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { git_gh_command } from '#scripts/git/git-gh-command'
4
4
  import { cli_body } from '#scripts/josh/cli-body'
5
+ import { error_text } from '#scripts/lib/error-message'
5
6
 
6
7
  // `josh issue:comment <N> --body <text> | --body-file <path>` — post one comment to an issue (a PR
7
8
  // comment is an issue comment over REST) and print the comment URL (joshuafolkken/kit#2304).
@@ -66,7 +67,7 @@ function resolve_request(argv: ReadonlyArray<string>): CommentRequest | undefine
66
67
 
67
68
  return request
68
69
  } catch (error) {
69
- console.error(error instanceof Error ? error.message : String(error))
70
+ console.error(error_text.message_of(error))
70
71
 
71
72
  return undefined
72
73
  }
@@ -7,6 +7,7 @@ import { git_gh_command } from '#scripts/git/git-gh-command'
7
7
  import { git_gh_exec } from '#scripts/git/git-gh-exec'
8
8
  import { git_gh_issue_write } from '#scripts/git/git-gh-issue-write'
9
9
  import { github_issue_url } from '#scripts/git/github-issue-url'
10
+ import { error_text } from '#scripts/lib/error-message'
10
11
  import { issue_file, type FileArguments } from './issue-file'
11
12
  import { issue_lint_cli } from './issue-lint-cli'
12
13
  import { issue_scout_cli } from './issue-scout-cli'
@@ -105,7 +106,7 @@ async function create(filing: Filing): Promise<string | undefined> {
105
106
  try {
106
107
  return await git_gh_exec.exec_gh_api(request)
107
108
  } catch (error) {
108
- const reason = error instanceof Error ? error.message : String(error)
109
+ const reason = error_text.message_of(error)
109
110
 
110
111
  console.error(`✖ the create call failed: ${reason}`)
111
112
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
- import { parseArgs } from 'node:util'
4
3
  import { git_command } from '#scripts/git/git-command'
5
4
  import { path_decision } from '#scripts/josh/path-decision'
5
+ import { cli_flags } from '#scripts/lib/cli-flags'
6
6
  import { split_assess, type SplitVerdict } from '#scripts/split/split-assess'
7
7
  import { issue_fold, type FoldVerdict } from './issue-fold'
8
8
 
@@ -33,27 +33,20 @@ interface FoldArguments {
33
33
  is_json: boolean
34
34
  }
35
35
 
36
- function parse_arguments(argv: ReadonlyArray<string>): FoldArguments {
37
- const { values, positionals } = parseArgs({
36
+ // An unknown flag is `undefined` rather than a throw, so the answer is the usage line rather than a
37
+ // stack trace, on a command whose output a workflow reads.
38
+ function read_arguments(argv: ReadonlyArray<string>): FoldArguments | undefined {
39
+ const parsed = cli_flags.parse_or_undefined({
38
40
  args: [...argv],
39
41
  options: { json: { type: 'boolean' }, 'not-separable': { type: 'boolean' } },
40
42
  allowPositionals: true,
41
43
  })
44
+ if (parsed === undefined) return undefined
42
45
 
43
46
  return {
44
- titles: positionals,
45
- is_separable: values['not-separable'] !== true,
46
- is_json: values.json === true,
47
- }
48
- }
49
-
50
- // An unknown flag makes `parseArgs` throw. Caught so the answer is the usage line rather than a stack
51
- // trace, on a command whose output a workflow reads.
52
- function read_arguments(argv: ReadonlyArray<string>): FoldArguments | undefined {
53
- try {
54
- return parse_arguments(argv)
55
- } catch {
56
- return undefined
47
+ titles: parsed.positionals,
48
+ is_separable: parsed.values['not-separable'] !== true,
49
+ is_json: parsed.values.json === true,
57
50
  }
58
51
  }
59
52
 
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
- import { parseArgs } from 'node:util'
5
4
  import { epic_audit_logic } from '#scripts/epic/epic-audit'
6
5
  import { epic_bundle, type BacklogIssue, type BundleDecision } from '#scripts/epic/epic-bundle'
7
6
  import { epic_bundle_cli } from '#scripts/epic/epic-bundle-cli'
8
7
  import { epic_bundle_gaps } from '#scripts/epic/epic-bundle-gaps'
9
8
  import { git_gh_command } from '#scripts/git/git-gh-command'
9
+ import { cli_flags } from '#scripts/lib/cli-flags'
10
10
  import { issue_citation } from '#scripts/rules/issue-citation'
11
11
  import {
12
12
  issue_scout,
@@ -59,12 +59,17 @@ function has_invalid_arguments(
59
59
  )
60
60
  }
61
61
 
62
- function parse_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
63
- const { values, positionals } = parseArgs({
62
+ // An unknown flag is `undefined` rather than a throw, so the answer is the usage line rather than a
63
+ // stack trace, on a command whose output a workflow reads.
64
+ function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
65
+ const parsed = cli_flags.parse_or_undefined({
64
66
  args: [...argv],
65
67
  options: { body: { type: 'string' }, 'body-file': { type: 'string' } },
66
68
  allowPositionals: true,
67
69
  })
70
+ if (parsed === undefined) return undefined
71
+
72
+ const { values, positionals } = parsed
68
73
  const [title] = positionals
69
74
 
70
75
  if (has_invalid_arguments(title, values.body, values['body-file'])) return undefined
@@ -83,16 +88,6 @@ async function with_draft_body(args: ScoutArguments): Promise<ScoutArguments | u
83
88
  }
84
89
  }
85
90
 
86
- // An unknown flag makes `parseArgs` throw. Caught so the answer is the usage line rather than a stack
87
- // trace, on a command whose output a workflow reads.
88
- function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined {
89
- try {
90
- return parse_arguments(argv)
91
- } catch {
92
- return undefined
93
- }
94
- }
95
-
96
91
  // The issue as it would be filed. The summary is carried as the body because that is what the epic
97
92
  // half reads: the issue numbers its prose names are the references `decide_bundle` decides from.
98
93
  function draft_of(args: ScoutArguments, repo: string): BacklogIssue {
@@ -2,6 +2,7 @@
2
2
  import { execSync } from 'node:child_process'
3
3
  import { readFile } from 'node:fs/promises'
4
4
  import { fileURLToPath } from 'node:url'
5
+ import { error_text } from '#scripts/lib/error-message'
5
6
  import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
6
7
  import { baseline_measure, type Baseline } from './baseline-measure'
7
8
 
@@ -21,7 +22,7 @@ function measure(command: string): string {
21
22
  try {
22
23
  return execSync(command, { encoding: 'utf8' }).trim()
23
24
  } catch (error) {
24
- return `(command failed: ${error instanceof Error ? error.message : String(error)})`
25
+ return `(command failed: ${error_text.message_of(error)})`
25
26
  }
26
27
  }
27
28
 
@@ -40,12 +41,12 @@ function rerun_one(baseline: Baseline, date: string): Outcome {
40
41
  }
41
42
  }
42
43
 
43
- async function append_ledger(lines: ReadonlyArray<string>): Promise<void> {
44
+ async function append_ledger(lines: ReadonlyArray<string>, now: Date): Promise<void> {
44
45
  if (lines.length === 0) return
45
46
 
46
- // The primary checkout's ledger even inside a lane, whose own copy never reaches the default branch
47
- // (joshuafolkken/kit#2419).
48
- const ledger_path = observation_ledger_home.ledger_path()
47
+ // The running work tree's file for the checked-out issue, a lane's inside a lane — the run's own
48
+ // commit carries it (joshuafolkken/kit#2919).
49
+ const ledger_path = await observation_ledger_home.writer_path(now)
49
50
 
50
51
  await observation_ledger_home.append(ledger_path, lines)
51
52
  console.info(`Recorded ${String(lines.length)} refuted premise(s) in ${ledger_path}.`)
@@ -70,7 +71,7 @@ async function rerun(body_path: string, now: Date): Promise<number> {
70
71
  .filter((line): line is string => line !== undefined)
71
72
 
72
73
  console.info(outcomes.map((outcome) => outcome.pair).join('\n\n'))
73
- await append_ledger(ledger_lines)
74
+ await append_ledger(ledger_lines, now)
74
75
 
75
76
  return 0
76
77
  }
@@ -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',
@@ -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']],
@@ -2,6 +2,7 @@
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 { error_text } from '#scripts/lib/error-message'
5
6
  import { run_carry } from '#scripts/run/run-carry'
6
7
  import { lane_await } from './lane-await'
7
8
  import { lane_close, type CloseOutcome, type SweepOutcome } from './lane-close'
@@ -384,7 +385,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
384
385
  try {
385
386
  return await dispatch(argv)
386
387
  } catch (error) {
387
- console.error(error instanceof Error ? error.message : String(error))
388
+ console.error(error_text.message_of(error))
388
389
 
389
390
  return FAILURE_EXIT_CODE
390
391
  }
@@ -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,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 }