@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,4 +1,5 @@
1
1
  import { agent_session_environment } from '#scripts/josh/agent-session-environment'
2
+ import { error_text } from '#scripts/lib/error-message'
2
3
  import { execa, execaSync } from 'execa'
3
4
  import { has_timed_out } from './git-execa-error'
4
5
  import { check_gh_installed } from './git-gh-check'
@@ -21,7 +22,7 @@ const BODY_FROM_STDIN = '-'
21
22
  // *sleeps* rather than the wall clock, so each poll's requests sit on top of it. A request allowed
22
23
  // to outlast that whole window would make the budget meaningless; half of it keeps one request
23
24
  // from dominating the look-ahead.
24
- // - kit's other `gh` spawns already sit at 5 seconds (`repo-setting.ts`) and 20
25
+ // - the readers that override it ask for 5 seconds (`repo-setting.ts`) and 20
25
26
  // (`propagate-publish.ts`, `epic-cross-repo.ts`). Sixty is deliberately the most generous of them,
26
27
  // because this entry also serves `--paginate` requests — several HTTP round trips inside one
27
28
  // spawn — which a 5-second budget would break.
@@ -70,10 +71,10 @@ function to_timeout_option(timeout_ms?: number): { timeout: number } {
70
71
  // Nothing is spread when there is nothing to remove, so a spawn outside such a wrapper keeps its
71
72
  // options exactly as they were.
72
73
  //
73
- // **It is exported because this file is not the only place `gh` is spawned.** The synchronous
74
- // readers that spell their own `gh api` (`gh-spawn.ts`, `repo-setting.ts`, `epic-cross-repo.ts`,
75
- // `version-remote.ts`, `propagate-publish.ts`) spread it too, and `git-gh-exec-proxy.test.ts` refuses
76
- // a new `gh api` spawn anywhere under `scripts/` that does not.
74
+ // **It is exported for the one spawn that is not a REST request** — `josh start`'s `gh repo create`
75
+ // step (`start-exec.ts`). Every `gh api` request goes through the entries below since
76
+ // joshuafolkken/kit#2901, which `gh-subcommand-guard.test.ts` enforces, and
77
+ // `git-gh-exec-proxy.test.ts` still refuses a `gh api` spawn that does not spread it.
77
78
  interface DirectEnvironment {
78
79
  env?: Record<string, undefined>
79
80
  }
@@ -84,8 +85,20 @@ function direct_environment(): DirectEnvironment {
84
85
  return Object.keys(removed).length > 0 ? { env: removed } : {}
85
86
  }
86
87
 
87
- function to_spawn_options(timeout_ms?: number): { timeout: number } & DirectEnvironment {
88
- return { ...to_timeout_option(timeout_ms), ...direct_environment() }
88
+ interface SpawnOptions extends DirectEnvironment {
89
+ timeout: number
90
+ cwd?: string
91
+ }
92
+
93
+ // `cwd` is spread in only when given: `{owner}/{repo}` in a path is resolved from the checkout gh
94
+ // runs in, so a caller pinned to the project root says so, and every other caller keeps inheriting
95
+ // the process's own directory exactly as before.
96
+ function to_spawn_options(timeout_ms?: number, cwd?: string): SpawnOptions {
97
+ return {
98
+ ...to_timeout_option(timeout_ms),
99
+ ...direct_environment(),
100
+ ...(cwd !== undefined && { cwd }),
101
+ }
89
102
  }
90
103
 
91
104
  // What a request that ran out of time is labelled with, ahead of whatever gh managed to write.
@@ -113,10 +126,6 @@ function to_timeout_prefix(error: unknown): string {
113
126
  return has_timed_out(error) ? `${GH_REQUEST_TIMEOUT_MESSAGE}: ` : ''
114
127
  }
115
128
 
116
- function to_error_message(error: unknown): string {
117
- return error instanceof Error ? error.message : String(error)
118
- }
119
-
120
129
  // Surface the gh CLI's stderr as the thrown message when present (matching the previous spawn
121
130
  // behavior), otherwise fall back to execa's own message — **and append what gh wrote to stdout**.
122
131
  //
@@ -134,7 +143,7 @@ function to_error_message(error: unknown): string {
134
143
  function to_gh_error(error: unknown): Error {
135
144
  const stderr = has_stderr_field(error) ? error.stderr.trim() : ''
136
145
  const stdout = has_stdout_field(error) ? error.stdout.trim() : ''
137
- const summary = stderr.length > 0 ? stderr : to_error_message(error)
146
+ const summary = stderr.length > 0 ? stderr : error_text.message_of(error)
138
147
  const detail = stdout.length > 0 ? `${summary}\n${stdout}` : summary
139
148
 
140
149
  return gh_failure.attach(
@@ -143,11 +152,16 @@ function to_gh_error(error: unknown): Error {
143
152
  )
144
153
  }
145
154
 
146
- async function exec_gh_command(arguments_: Array<string>, timeout_ms?: number): Promise<string> {
155
+ async function exec_gh_command(
156
+ arguments_: Array<string>,
157
+ timeout_ms?: number,
158
+ cwd?: string,
159
+ ): Promise<string> {
147
160
  await check_gh_installed()
161
+ const options = to_spawn_options(timeout_ms, cwd)
148
162
 
149
163
  try {
150
- const { stdout } = await execa('gh', arguments_, to_spawn_options(timeout_ms)) // NOSONAR S8705: execa array args (no shell), trusted dev CLI tooling
164
+ const { stdout } = await execa('gh', arguments_, options) // NOSONAR S8705: execa array args (no shell), trusted dev CLI tooling
151
165
 
152
166
  return stdout.trimEnd()
153
167
  } catch (error) {
@@ -161,12 +175,13 @@ async function exec_gh_command_with_stdin(input: {
161
175
  // `number | undefined` rather than plain optional: `exec_gh_api` forwards its own optional field,
162
176
  // which `exactOptionalPropertyTypes` will not narrow for it.
163
177
  timeout_ms?: number | undefined
178
+ cwd?: string | undefined
164
179
  }): Promise<string> {
165
180
  await check_gh_installed()
166
181
 
167
182
  // Built ahead of the call so the spawn itself stays on one line: `// NOSONAR` suppresses the rule
168
183
  // only on the line it sits on, and a wrapped call moves that line away from the reported one.
169
- const options = { input: input.stdin_body, ...to_spawn_options(input.timeout_ms) }
184
+ const options = { input: input.stdin_body, ...to_spawn_options(input.timeout_ms, input.cwd) }
170
185
 
171
186
  try {
172
187
  const { stdout } = await execa('gh', input.args, options) // NOSONAR S8705: execa array args (no shell), trusted dev CLI tooling
@@ -186,6 +201,12 @@ function parse_status_line(output: string): number | undefined {
186
201
  return status === undefined ? undefined : Number(status)
187
202
  }
188
203
 
204
+ // What a status probe that exited non-zero reached: `--include` keeps the status line even then,
205
+ // and execa carries it on the error it throws.
206
+ function status_of_failure(error: unknown): number | undefined {
207
+ return has_stdout_field(error) ? parse_status_line(error.stdout) : undefined
208
+ }
209
+
189
210
  // The HTTP status of one `gh api` request, read from the status line rather than from `gh`'s error
190
211
  // text. A message is prose that can be reworded between releases; the status code is the protocol,
191
212
  // so a caller distinguishing "there is nothing at that number" (404) from "the read failed" (403,
@@ -215,7 +236,24 @@ async function exec_gh_api_status(path: string, timeout_ms?: number): Promise<nu
215
236
 
216
237
  return parse_status_line(stdout)
217
238
  } catch (error) {
218
- return has_stdout_field(error) ? parse_status_line(error.stdout) : undefined
239
+ return status_of_failure(error)
240
+ }
241
+ }
242
+
243
+ // The synchronous twin of `exec_gh_api_status`, for `epic-cross-repo.ts`, whose resolver is
244
+ // synchronous all the way up to `epic_classify` (joshuafolkken/kit#2901). Same request, same
245
+ // answer: the status line, or `undefined` when no status was reached. It cannot run
246
+ // `check_gh_installed`, which awaits — a missing gh is a spawn that throws without stdout, so it
247
+ // answers `undefined`, which is what that check would have led to anyway.
248
+ function exec_gh_api_status_sync(path: string, timeout_ms?: number): number | undefined {
249
+ const options = to_spawn_options(timeout_ms)
250
+
251
+ try {
252
+ const { stdout } = execaSync('gh', ['api', '--include', '--silent', path], options) // NOSONAR S8705: execa array args (no shell), trusted dev CLI tooling
253
+
254
+ return parse_status_line(stdout)
255
+ } catch (error) {
256
+ return status_of_failure(error)
219
257
  }
220
258
  }
221
259
 
@@ -251,8 +289,13 @@ interface GhApiRequest {
251
289
  should_slurp?: boolean
252
290
  jq_filter?: string
253
291
  // The budget for this one request, overriding `GH_REQUEST_TIMEOUT_MS`. A field rather than a
254
- // second argument for the reason recorded beside that constant (joshuafolkken/kit#1065).
292
+ // second argument for the reason recorded beside that constant (joshuafolkken/kit#1065). `0` is
293
+ // execa's own "no budget", kept for the one reader whose caller's writes depend on the answer
294
+ // (`gh-spawn.ts`, joshuafolkken/kit#805).
255
295
  timeout_ms?: number
296
+ // The checkout gh runs in, which is what `{owner}/{repo}` in `path` resolves against. Absent means
297
+ // the process's own directory.
298
+ cwd?: string
256
299
  }
257
300
 
258
301
  function optional_flag(flag: string, value?: string): Array<string> {
@@ -277,20 +320,23 @@ function to_gh_api_args(request: GhApiRequest): Array<string> {
277
320
 
278
321
  // The response body of one `gh api` request, as text. Every REST caller that can await one goes
279
322
  // through it, so the verb, the body and the paging stop being spelled out per call site
280
- // (joshuafolkken/kit#1023). The synchronous readers that cannot — `gh-spawn.ts`, `epic-cross-repo.ts`
281
- // — still share its path builder.
323
+ // (joshuafolkken/kit#1023). A caller that cannot await uses `exec_gh_api_sync` or
324
+ // `read_gh_api_sync` below.
282
325
  //
283
326
  // Failure handling funnels through `to_gh_error` on both paths below, so a failed request throws
284
327
  // with gh's stderr summary *and* the JSON error body it wrote to stdout.
285
328
  async function exec_gh_api(request: GhApiRequest): Promise<string> {
286
329
  const args = to_gh_api_args(request)
287
330
 
288
- if (request.body === undefined) return await exec_gh_command(args, request.timeout_ms)
331
+ if (request.body === undefined) {
332
+ return await exec_gh_command(args, request.timeout_ms, request.cwd)
333
+ }
289
334
 
290
335
  return await exec_gh_command_with_stdin({
291
336
  args,
292
337
  stdin_body: request.body,
293
338
  timeout_ms: request.timeout_ms,
339
+ cwd: request.cwd,
294
340
  })
295
341
  }
296
342
 
@@ -300,11 +346,10 @@ async function exec_gh_api(request: GhApiRequest): Promise<string> {
300
346
  // opens in the consumer is created here rather than by turning the whole runner asynchronous for
301
347
  // one request (joshuafolkken/kit#1042).
302
348
  //
303
- // It is the **write** side's synchronous entry, and the only one so far. The synchronous *readers*
304
- // named above `exec_gh_api` — `gh-spawn.ts`, `epic-cross-repo.ts`, `repo-setting.ts`,
305
- // `propagate-publish.ts`, `version-remote.ts` — each spell their own `['api', path, '--jq', …]` out
306
- // and share only the path builder; a read has no body and no verb to get wrong, which is why they
307
- // were left as they are rather than migrated here in passing.
349
+ // It began as the write side's synchronous entry; since joshuafolkken/kit#2901 the synchronous
350
+ // readers — `gh-spawn.ts`, `epic-cross-repo.ts`, `repo-setting.ts`, `propagate-publish.ts`,
351
+ // `version-remote.ts` — go through it too, rather than each spelling its own `execaSync('gh', …)`
352
+ // with its own error handling, timeout and proxy handling.
308
353
  //
309
354
  // **It is the same layer, not a second one.** The argument builder and the error translation are
310
355
  // shared with the asynchronous path above, so `--input -` for the body, `--jq` for the unwrap, and
@@ -318,7 +363,7 @@ async function exec_gh_api(request: GhApiRequest): Promise<string> {
318
363
  // replaced already did.
319
364
  function exec_gh_api_sync(request: GhApiRequest): string {
320
365
  const body_option = request.body === undefined ? {} : { input: request.body }
321
- const options = { ...body_option, ...to_spawn_options(request.timeout_ms) }
366
+ const options = { ...body_option, ...to_spawn_options(request.timeout_ms, request.cwd) }
322
367
 
323
368
  try {
324
369
  const { stdout } = execaSync('gh', to_gh_api_args(request), options) // NOSONAR S8705: execa array args (no shell), trusted dev CLI tooling
@@ -329,13 +374,27 @@ function exec_gh_api_sync(request: GhApiRequest): string {
329
374
  }
330
375
  }
331
376
 
377
+ // `exec_gh_api_sync` for a reader to which a failed request is an answer rather than an error:
378
+ // `undefined` for every failure — a non-zero exit, a timeout, gh missing — and the response text
379
+ // otherwise. The readers that degrade instead of throwing (a report that says `unreadable`, a
380
+ // registry read that retries) share this one catch rather than each writing their own.
381
+ function read_gh_api_sync(request: GhApiRequest): string | undefined {
382
+ try {
383
+ return exec_gh_api_sync(request)
384
+ } catch {
385
+ return undefined
386
+ }
387
+ }
388
+
332
389
  const git_gh_exec = {
333
390
  direct_environment,
334
391
  exec_gh_command,
335
392
  exec_gh_command_with_stdin,
336
393
  exec_gh_api,
337
394
  exec_gh_api_sync,
395
+ read_gh_api_sync,
338
396
  exec_gh_api_status,
397
+ exec_gh_api_status_sync,
339
398
  parse_status_line,
340
399
  }
341
400
 
@@ -1,4 +1,3 @@
1
- import type { IssueReference } from './git-epic-reference'
2
1
  import { git_gh_api_path } from './git-gh-api-path'
3
2
  import { git_gh_exec } from './git-gh-exec'
4
3
  import { gh_failure, type GhFailure } from './git-gh-failure'
@@ -10,6 +9,7 @@ import {
10
9
  type RestIssue,
11
10
  } from './git-gh-issue-rest'
12
11
  import { gh_reachability } from './git-gh-reachability'
12
+ import type { IssueReference } from './issue-reference'
13
13
 
14
14
  // Reading one issue. Split out of `git-gh-issue.ts`, which had grown past the file-length limit
15
15
  // while holding both the reads and the writes; the reads are what every epic command goes through,
@@ -341,7 +341,7 @@ async function issue_get_plan_fields_classified(issue_number: string): Promise<I
341
341
  return await issue_view_json_classified(issue_number, PLAN_FIELDS)
342
342
  }
343
343
 
344
- // `state` rides along for `git-epic-add.ts`'s closed-epic refusal (joshuafolkken/kit#2337); every
344
+ // `state` rides along for `epic-add.ts`'s closed-epic refusal (joshuafolkken/kit#2337); every
345
345
  // other caller reads only the labels and body and ignores it, so it costs no extra request.
346
346
  async function issue_get_labels_and_body(issue_number: string): Promise<string | undefined> {
347
347
  return await issue_view_json(issue_number, 'number,state,labels,body')
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
- import type { IssueReference } from './git-epic-reference'
3
2
  import { MERGED_STATE, to_gh_state } from './git-gh-rest-state'
3
+ import type { IssueReference } from './issue-reference'
4
4
  import { parse_json_array_or_undefined, parse_json_object_safe } from './parse-json-array'
5
5
  import { blocking_issue_schema } from './schemas'
6
6
 
@@ -19,7 +19,7 @@ import { blocking_issue_schema } from './schemas'
19
19
  //
20
20
  // `number` is required so that a 200 carrying something other than an issue — a proxy's HTML error
21
21
  // page, an API message object — fails the parse rather than passing as an issue whose every field is
22
- // missing. `git-epic-close` would turn that into `is_closed: false` for every child and report it as
22
+ // missing. `epic-close` would turn that into `is_closed: false` for every child and report it as
23
23
  // fact. The dependency summary is absent on a pull request, which the issue endpoint serves as
24
24
  // readily as an issue, so it stays optional and tolerates an explicit null.
25
25
  const rest_issue_schema = z.looseObject({
@@ -24,7 +24,7 @@ const PATCH_METHOD = 'PATCH'
24
24
  const DELETE_METHOD = 'DELETE'
25
25
 
26
26
  // `gh issue edit`, `gh issue comment` and `gh issue create` each printed one URL, and the callers
27
- // read it — `git-epic-run.ts` parses the epic's number back out of it. REST answers the whole
27
+ // read it — `epic-run.ts` parses the epic's number back out of it. REST answers the whole
28
28
  // object, so the same value is unwrapped out of it rather than the shape being changed.
29
29
  const HTML_URL_FILTER = '.html_url'
30
30
  const ID_FILTER = '.id'
@@ -34,9 +34,9 @@ const COMMENTS_SEGMENT = '/comments'
34
34
  const CLOSED_STATE = 'closed'
35
35
  const COLOR_HASH = '#'
36
36
 
37
- // The four writers below answered `boolean` under `gh`, and their callers read it: `git-epic-run.ts`
38
- // only reports the epic label when it was applied, `git-epic-relations.ts` counts the relations it
39
- // could not apply, and `git-epic-close.ts` prints "close it manually" on a false. One try/catch
37
+ // The four writers below answered `boolean` under `gh`, and their callers read it: `epic-run.ts`
38
+ // only reports the epic label when it was applied, `epic-relations.ts` counts the relations it
39
+ // could not apply, and `epic-close.ts` prints "close it manually" on a false. One try/catch
40
40
  // rather than four copies of it (`CLAUDE.md` → "No clones").
41
41
  async function did_write_succeed(write: () => Promise<unknown>): Promise<boolean> {
42
42
  try {
@@ -77,7 +77,7 @@ async function issue_try_comment(issue_number: string, body: string): Promise<bo
77
77
  // **The comment goes first**, which is what keeps the return value meaning what it meant: a `false`
78
78
  // then says "the issue is still open" in *both* failure branches, because the state change is the
79
79
  // last thing attempted. Closing first would answer `false` for an issue that is in fact closed, and
80
- // `git-epic-close.ts` prints "close it manually" on that answer.
80
+ // `epic-close.ts` prints "close it manually" on that answer.
81
81
  //
82
82
  // **`comment: undefined` closes without commenting**, and it is what the ordering above costs: a run
83
83
  // whose comment landed and whose close was refused leaves the issue open carrying the comment, so
@@ -168,7 +168,7 @@ function issue_create_request(input: {
168
168
  }
169
169
 
170
170
  // The browser URL of the created issue, which is what `gh issue create` printed and what
171
- // `git-epic-run.ts` parses the epic's number out of.
171
+ // `epic-run.ts` parses the epic's number out of.
172
172
  async function issue_create_with_label(input: {
173
173
  title: string
174
174
  label: string
@@ -43,9 +43,6 @@ const COMMENTS_QUERY = FULL_PAGE_QUERY
43
43
  // Written as a constant rather than inline: `{owner}` inside a template literal reads as a broken
44
44
  // interpolation.
45
45
  const OWNER_PLACEHOLDER = '{owner}'
46
- const NO_HEAD_REF_MESSAGE = 'gh api answered a pull request with no head branch'
47
- const FORK_HEAD_MESSAGE =
48
- 'gh api answered a pull request whose head branch is in another repository'
49
46
  // What a branch-keyed caller that cannot fold an absence into its own answer throws. Lives here
50
47
  // rather than beside either caller: the merge-gate snapshot and the two branch-keyed writes all need
51
48
  // it, and three copies of one message is the clone `CLAUDE.md` prohibits (joshuafolkken/kit#1029).
@@ -236,21 +233,6 @@ async function pr_view(branch_name: string): Promise<string> {
236
233
  return JSON.stringify(git_gh_pr_rest.to_pr_info(pull))
237
234
  }
238
235
 
239
- // Already keyed by number, so no resolution is needed. It throws where the others fold to an empty
240
- // answer, which is the contract `sync-dependabot-pins.ts` relies on: it checks out the branch it is
241
- // handed, and a guessed one would push template pins onto the wrong PR.
242
- async function pr_head_reference(pr_number: number): Promise<string> {
243
- const pull = await read_pull(pr_number)
244
- const reference = pull.head?.ref
245
- if (reference === undefined) throw new Error(NO_HEAD_REF_MESSAGE)
246
- // A fork's head is not on `origin` under this name, and `origin` may well carry a different branch
247
- // that answers to it. Refusing is the same contract the missing ref has: the caller checks out what
248
- // it is handed, so a guessed branch pushes template pins onto the wrong pull request.
249
- if (!git_gh_pr_rest.is_same_repository_head(pull)) throw new Error(FORK_HEAD_MESSAGE)
250
-
251
- return reference
252
- }
253
-
254
236
  // `undefined` when the listing could not be read — a failed request, or a PR whose number could not
255
237
  // be resolved. Not `'[]'`, which every failure used to become.
256
238
  //
@@ -300,7 +282,6 @@ const git_gh_pr_read = {
300
282
  pr_get_url,
301
283
  pr_get_body,
302
284
  pr_view,
303
- pr_head_reference,
304
285
  pr_get_comments,
305
286
  pr_get_review_comments,
306
287
  }
@@ -315,8 +296,6 @@ export {
315
296
  read_pull,
316
297
  require_pr_number,
317
298
  resolve_pr_number,
318
- FORK_HEAD_MESSAGE,
319
- NO_HEAD_REF_MESSAGE,
320
299
  NO_PULL_REQUEST_MESSAGE,
321
300
  UNREADABLE_PULL_REQUEST_MESSAGE,
322
301
  }
@@ -21,7 +21,8 @@ import { git_gh_pr_snapshot } from './git-gh-pr-snapshot'
21
21
  // **Every path here was measured against the live API before it was written**, on a throwaway pull
22
22
  // request opened between two throwaway branches so that neither `main` nor any real pull request was
23
23
  // touched. Two of the four did not behave as reading the documentation would suggest: see
24
- // `pr_create` for the duplicate-head response and `pr_checkout` for what `gh` actually does.
24
+ // `pr_create` for the duplicate-head response. The other, `pr_checkout`, is gone with its one caller,
25
+ // `sync-dependabot-pins` (joshuafolkken/kit#2887).
25
26
  //
26
27
  // `pr_checks` — a `gh pr checks <branch>` wrapper — is gone rather than converted. Nothing called it:
27
28
  // the merge gate reads the rollup out of the snapshot and `git-pr-checks-watch.ts` runs the watching
@@ -139,27 +140,6 @@ async function pr_create(
139
140
  return url
140
141
  }
141
142
 
142
- // **`gh pr checkout` does use GraphQL** — one `POST /graphql` to resolve the pull request, measured
143
- // with `GH_DEBUG=api` on the throwaway pull request (joshuafolkken/kit#1029). Everything after that
144
- // is git, so only the resolution had to move: `pr_head_reference` is the REST read that already
145
- // replaced it for this file's one caller, `sync-dependabot-pins.ts`.
146
- //
147
- // `fetch_branch` updates `refs/remotes/origin/<branch>`, which is what lets the plain `checkout`
148
- // resolve a branch that exists only on the remote — the tracking branch `gh` left behind, by the
149
- // operations `git-command.ts` already owns rather than a new wrapper. The fast-forward is the third
150
- // thing the CLI did and the one a fetch-and-checkout pair silently drops: a branch that is already
151
- // local is checked out at whatever commit the last run left it on.
152
- //
153
- // A fork's head is refused by `pr_head_reference` rather than fetched from `origin` under a name
154
- // that repository may use for something else.
155
- async function pr_checkout(pr_number: number): Promise<void> {
156
- const branch_name = await git_gh_pr_read.pr_head_reference(pr_number)
157
-
158
- await git_command.fetch_branch(branch_name)
159
- await git_command.checkout(branch_name)
160
- await git_command.merge_fast_forward(branch_name)
161
- }
162
-
163
143
  // A pull request's conversation comment is an **issue** comment: `POST issues/{N}/comments` is the
164
144
  // endpoint, and it answers the same `html_url` for a pull request as for an issue (measured on the
165
145
  // throwaway pull request). `git-gh-issue-write.ts` already posts to it, so this resolves the number
@@ -270,7 +250,6 @@ const git_gh_pr = {
270
250
  pr_create,
271
251
  pr_get_classification,
272
252
  pr_ensure_classification,
273
- pr_checkout,
274
253
  pr_comment,
275
254
  pr_merge,
276
255
  pr_update_body,
@@ -1,6 +1,5 @@
1
- import { git_epic_close } from './git-epic-close'
1
+ import { epic_close } from '#scripts/epic/epic-close'
2
2
  import { git_followup_cleanup } from './git-followup-cleanup'
3
- import { git_followup_flush } from './git-followup-flush'
4
3
  import { git_followup_issue_close } from './git-followup-issue-close'
5
4
  import { git_followup_label } from './git-followup-label'
6
5
  import { git_followup_stages, type StageLog } from './git-followup-stages'
@@ -137,7 +136,7 @@ async function epic_close_step(input: WrapupInput): Promise<void> {
137
136
  label: 'The epic auto-close',
138
137
  recovery: undefined,
139
138
  run: async () => {
140
- await git_epic_close.close_completed_epics({
139
+ await epic_close.close_completed_epics({
141
140
  issue_number: input.issue_number,
142
141
  is_merged: input.should_merge,
143
142
  })
@@ -234,9 +233,6 @@ async function run_wrapup(input: WrapupInput, log: StageLog): Promise<void> {
234
233
  }
235
234
 
236
235
  await run_tail_steps(input)
237
- // After the merge tail and before the hold is released (`git-followup-finish.ts`): the ledger
238
- // flush switches branches, so it must land while this run still holds the tree (kit#1810).
239
- await git_followup_flush.flush_ledger_step(input.should_merge)
240
236
 
241
237
  lap(log, STAGE.completion_and_epic_close)
242
238
  }
@@ -1,3 +1,4 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { live_evidence } from '#scripts/review/live-evidence'
2
3
  import { changed_paths } from './changed-paths'
3
4
  import { git_branch } from './git-branch'
@@ -28,10 +29,6 @@ interface Resolution {
28
29
  problems: Array<string>
29
30
  }
30
31
 
31
- function error_message(error: unknown): string {
32
- return error instanceof Error ? error.message : String(error)
33
- }
34
-
35
32
  function resolve_from_branch(current_branch: string): Resolution {
36
33
  const number = git_issue.branch_number(current_branch)
37
34
 
@@ -71,7 +68,7 @@ function resolve_from_title(
71
68
  try {
72
69
  return resolve_parsed(git_issue.parse(cli_input), current_branch, default_branch)
73
70
  } catch (error) {
74
- return { target: undefined, problems: [error_message(error)] }
71
+ return { target: undefined, problems: [error_text.message_of(error)] }
75
72
  }
76
73
  }
77
74
 
@@ -89,7 +86,7 @@ async function classification_problems(target: Target): Promise<Array<string>> {
89
86
 
90
87
  return []
91
88
  } catch (error) {
92
- return [error_message(error)]
89
+ return [error_text.message_of(error)]
93
90
  }
94
91
  }
95
92
 
@@ -1,6 +1,7 @@
1
1
  import {
2
+ LEGACY_OBSERVATION_LEDGER_PATHS,
2
3
  observation_ledger,
3
- OBSERVATION_LEDGER_PATHS,
4
+ OBSERVATION_LEDGER_DIRECTORY,
4
5
  } from '#scripts/observations/observation-ledger'
5
6
  import { observation_ledger_prepare } from '#scripts/observations/observation-ledger-prepare'
6
7
  import { git_command } from './git-command'
@@ -77,17 +78,28 @@ async function stage_untracked_files(files: ReadonlyArray<string>): Promise<void
77
78
  // **A line that breaks the grammar keeps the old exclusion** (joshuafolkken/kit#2123): the commit
78
79
  // goes ahead without the ledger, and `pnpm josh observations:flush` — which refuses the same line and
79
80
  // names it — is where it is repaired. The migration is run first, as the flush runs it, so a line
80
- // still on the old path (joshuafolkken/kit#2724) is committed at the new one. A lane has no ledger
81
- // line of its own to carry: its writers append to the primary checkout's.
81
+ // still on an old path (joshuafolkken/kit#2724) is committed in the directory. **A lane carries its
82
+ // own lines** (joshuafolkken/kit#2919): its writers append to its own tree's issue file, so this is
83
+ // the step that takes them to the default branch.
84
+ const LEDGER_PATHSPECS: ReadonlyArray<string> = [
85
+ OBSERVATION_LEDGER_DIRECTORY,
86
+ ...LEGACY_OBSERVATION_LEDGER_PATHS,
87
+ ]
88
+
82
89
  async function ledger_exclusions(): Promise<ReadonlyArray<string>> {
83
90
  const broken = await observation_ledger_prepare.prepare(await git_command.repository_root())
84
91
 
85
- return broken.length > 0 ? OBSERVATION_LEDGER_PATHS : []
92
+ return broken.length > 0 ? LEDGER_PATHSPECS : []
93
+ }
94
+
95
+ // An excluded pathspec covers what sits under it, as git reads it — the directory's files included.
96
+ function is_excluded(file_path: string, excluded: ReadonlyArray<string>): boolean {
97
+ return excluded.some((spec) => file_path === spec || file_path.startsWith(`${spec}/`))
86
98
  }
87
99
 
88
100
  // A migration claim is a copy mid-move, never the ledger itself, so it is never staged.
89
101
  function is_stageable(file_path: string, excluded: ReadonlyArray<string>): boolean {
90
- return !observation_ledger.is_migration_claim(file_path) && !excluded.includes(file_path)
102
+ return !observation_ledger.is_migration_claim(file_path) && !is_excluded(file_path, excluded)
91
103
  }
92
104
 
93
105
  // **Said out loud, because the alternative is an unexplained failure.** With the ledger the only
@@ -95,15 +107,15 @@ function is_stageable(file_path: string, excluded: ReadonlyArray<string>): boole
95
107
  // step dies on git's empty index with `Failed to commit changes` — a message naming nothing that
96
108
  // caused it. This line is what turns that into a diagnosis.
97
109
  //
98
- // **The paths are parsed rather than matched as substrings**: `docs/maintainers/observations.md.bak` contains the
99
- // ledger's path, and a hint naming a file the exclusion never touched is a hint that teaches the
100
- // reader to ignore it.
110
+ // **The paths are parsed rather than matched as substrings**: `docs/maintainers/observations-old/x.md`
111
+ // contains the ledger's path, and a hint naming a file the exclusion never touched is a hint that
112
+ // teaches the reader to ignore it.
101
113
  function report_excluded_paths(status_output: string, excluded: ReadonlyArray<string>): void {
102
114
  const left_out = status_output
103
115
  .split('\n')
104
116
  .filter((line) => line.trim().length > 0)
105
117
  .map((line) => observation_ledger.status_path(line))
106
- .filter((file_path) => excluded.includes(file_path))
118
+ .filter((file_path) => is_excluded(file_path, excluded))
107
119
 
108
120
  for (const file_path of left_out) {
109
121
  console.info(
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { parseArgs } from 'node:util'
3
- import { git_branch } from '../scripts/git/git-branch'
4
- import { git_commit } from '../scripts/git/git-commit'
5
- import { git_error } from '../scripts/git/git-error'
6
- import { git_issue, type IssueInfo } from '../scripts/git/git-issue'
7
- import { git_pr } from '../scripts/git/git-pr'
8
- import { git_preflight } from '../scripts/git/git-preflight'
9
- import { git_prompt, type WorkflowConfirmations } from '../scripts/git/git-prompt'
10
- import { git_push } from '../scripts/git/git-push'
11
- import { git_staging } from '../scripts/git/git-staging'
12
- import { cli_body } from '../scripts/josh/cli-body'
13
- import { test_red_commit } from '../scripts/test/test-red-commit'
3
+ import { cli_body } from '#scripts/josh/cli-body'
4
+ import { test_red_commit } from '#scripts/test/test-red-commit'
5
+ import { git_branch } from './git-branch'
6
+ import { git_commit } from './git-commit'
7
+ import { git_error } from './git-error'
8
+ import { git_issue, type IssueInfo } from './git-issue'
9
+ import { git_pr } from './git-pr'
10
+ import { git_preflight } from './git-preflight'
11
+ import { git_prompt, type WorkflowConfirmations } from './git-prompt'
12
+ import { git_push } from './git-push'
13
+ import { git_staging } from './git-staging'
14
14
 
15
15
  const SKIP_MESSAGES = {
16
16
  commit: '💡 Commit skipped.',
@@ -6,7 +6,7 @@
6
6
  // notification carrying only a pull-request URL until joshuafolkken/kit#994 added the second form
7
7
  // here rather than beside it.
8
8
  //
9
- // Two other modules read a URL for something narrower and keep their own patterns. `git-epic-parse`
9
+ // Two other modules read a URL for something narrower and keep their own patterns. `epic-parse`
10
10
  // scans an epic body for task-list rows, so its regex is anchored to the row marker and runs
11
11
  // globally over a document; `propagate-steps` takes the trailing number of whatever URL
12
12
  // `gh issue create` just printed, where the repository is already known. This one is for a caller
@@ -282,7 +282,7 @@ function label_name_of(labels: ReadonlyArray<string>, wanted: string): string |
282
282
 
283
283
  // The same comparison for a caller holding label *names* rather than listing rows. `EpicChild.labels`
284
284
  // is an array of strings (`scripts/epic/epic-graph.ts`), which is the one shape `has_any_label` cannot
285
- // take — so `epic-classify.ts` and `git-epic-validate.ts` each grew a raw case-sensitive
285
+ // take — so `epic-classify.ts` and `epic-validate.ts` each grew a raw case-sensitive
286
286
  // `Array.includes` instead, and an `Epic`-cased label walked past both. Kept here beside the rule it
287
287
  // implements rather than at either call site, for the reason the comment above gives.
288
288
  //
@@ -0,0 +1,19 @@
1
+ // An issue named from outside any one graph — a task-list row, a citation in prose, a blocker
2
+ // relation, a child that could not be read. Repository **and** number, because a number alone cannot
3
+ // identify one: issue numbers are unique per repository, so the same `#40` names two different issues
4
+ // depending on who wrote it (joshuafolkken/kit#1014).
5
+ //
6
+ // It lives in the git layer because the `blocked-by` relations are read here and carry a repository
7
+ // of their own (joshuafolkken/kit#1126); the epic domain builds on this layer, never the other way
8
+ // round (joshuafolkken/kit#2904). `epic-reference` re-exports it, so every epic importer is unchanged.
9
+ //
10
+ // `state` is present only on a reference read from a `blocked-by` relation whose response carried it
11
+ // (joshuafolkken/kit#1943). The classifier needs it for a blocker no named graph tracks, where there is
12
+ // no child record to read the state from; every other reference leaves it out.
13
+ interface IssueReference {
14
+ repo: string
15
+ number: number
16
+ state?: 'OPEN' | 'CLOSED'
17
+ }
18
+
19
+ export type { IssueReference }
@@ -2,6 +2,7 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { extract_issue_number } from '#scripts/hooks/check-commit-message'
4
4
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
5
+ import { error_text } from '#scripts/lib/error-message'
5
6
  import { git_command } from './git-command'
6
7
  import { main_merge_guard } from './main-merge-guard'
7
8
 
@@ -103,7 +104,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
103
104
  try {
104
105
  return await dispatch(argv)
105
106
  } catch (error) {
106
- console.error(error instanceof Error ? error.message : String(error))
107
+ console.error(error_text.message_of(error))
107
108
 
108
109
  return FAILURE_EXIT_CODE
109
110
  }