@joshuafolkken/kit 1.1010.0 → 1.1050.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/.claude/settings.json +1 -5
  2. package/.claude/skills/dependency-update/SKILL.md +6 -3
  3. package/.claude/skills/epic-commands/SKILL.md +46 -17
  4. package/.claude/skills/verify-ui/SKILL.md +1 -1
  5. package/.claude/skills/workflow-commands/SKILL.md +10 -25
  6. package/.claude/skills/workflow-commands/background-commands.md +1 -1
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +6 -4
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +1 -1
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +6 -0
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +30 -36
  11. package/.claude/skills/workflow-commands/backlogrun-steps.md +16 -16
  12. package/.claude/skills/workflow-commands/backlogrun.md +3 -12
  13. package/.claude/skills/workflow-commands/chain-rule.md +2 -2
  14. package/.claude/skills/workflow-commands/followup-reference.md +2 -2
  15. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
  16. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  17. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  18. package/.claude/skills/workflow-commands/into-target.md +1 -1
  19. package/.claude/skills/workflow-commands/issue-comments.md +1 -1
  20. package/.claude/skills/workflow-commands/observation-filing.md +80 -126
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +33 -36
  22. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  23. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -2
  24. package/.codex/hooks.json +1 -5
  25. package/.github/workflows/auto-tag.yml +21 -0
  26. package/.github/workflows/publish.yml +9 -10
  27. package/.github/workflows/sonar-qube.yml +1 -1
  28. package/AGENTS.md +4 -25
  29. package/CLAUDE.md +24 -38
  30. package/GEMINI.md +4 -24
  31. package/README.md +22 -56
  32. package/dist/CLAUDE.md +24 -38
  33. package/dist/hooks/batch-guard.js +4 -4
  34. package/dist/hooks/{chunk-6PSTVOLH.js → chunk-5BK3K7V4.js} +114 -90
  35. package/dist/hooks/{chunk-PAXMLGSS.js → chunk-7VPDUBRN.js} +93 -175
  36. package/dist/hooks/{chunk-KPZN42RT.js → chunk-DMQN45FN.js} +3 -3
  37. package/dist/hooks/{chunk-WMIR5SNX.js → chunk-EJDX2TOB.js} +13 -3
  38. package/dist/hooks/{chunk-A2H5I2VD.js → chunk-GQ7KP45M.js} +2 -2
  39. package/dist/hooks/{chunk-4GDR2YTV.js → chunk-HJIISEAC.js} +4 -4
  40. package/dist/hooks/{chunk-AWTFKKFW.js → chunk-NAS4C7GP.js} +6 -33
  41. package/dist/hooks/{chunk-56LGMJJV.js → chunk-NJQOBE6L.js} +2 -2
  42. package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-RLDX5SOC.js} +1222 -941
  43. package/dist/hooks/{chunk-GBJUERYK.js → chunk-WQ4OSBSS.js} +10 -3
  44. package/dist/hooks/codex-hook-adapter.js +10 -10
  45. package/dist/hooks/duplicate-read-guard.js +5 -5
  46. package/dist/hooks/format-edited.js +15 -34
  47. package/dist/hooks/investigation-guard.js +5 -5
  48. package/dist/hooks/pretool-guard.js +23 -16
  49. package/dist/hooks/stop-guard.js +21 -200
  50. package/dist/josh.js +42 -125
  51. package/dist/version/index.js +466 -42
  52. package/eslint/base.js +2 -11
  53. package/package.json +36 -10
  54. package/prompts/coding-standards.md +15 -290
  55. package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
  56. package/prompts/collaboration-workflow/epic-audit.md +2 -53
  57. package/prompts/collaboration-workflow/epic-bundle.md +2 -106
  58. package/prompts/collaboration-workflow/file-edits.md +6 -53
  59. package/prompts/collaboration-workflow/issue-template.md +4 -4
  60. package/prompts/collaboration-workflow/operating-rules.md +18 -75
  61. package/prompts/collaboration-workflow/principles.md +74 -0
  62. package/prompts/collaboration-workflow/residency.md +55 -15
  63. package/prompts/collaboration-workflow/rule-delivery.md +5 -46
  64. package/prompts/collaboration-workflow/shell-body.md +7 -9
  65. package/prompts/collaboration-workflow/turn-batching.md +21 -37
  66. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  67. package/prompts/collaboration-workflow/wip-cap.md +15 -18
  68. package/prompts/collaboration-workflow.md +22 -28
  69. package/prompts/sonar-hotspot-handling.md +1 -1
  70. package/scripts/adopt/adopt-logic.ts +2 -2
  71. package/scripts/adopt/adopt-toolkits.ts +3 -1
  72. package/scripts/adopt/adopt.ts +2 -2
  73. package/scripts/agent/agent-event.ts +2 -9
  74. package/scripts/agent/agent-exit-record.ts +2 -9
  75. package/scripts/auto-ok/auto-ok-cli.ts +9 -9
  76. package/scripts/backlog/backlog-budget-cli.ts +2 -6
  77. package/scripts/backlog/backlog-drive-cli.ts +20 -10
  78. package/scripts/backlog/backlog-drive.ts +50 -16
  79. package/scripts/backlog/backlog-next.ts +24 -8
  80. package/scripts/backlog/backlog-offer-cli.ts +6 -5
  81. package/scripts/backlog/backlog-plan-cli.ts +5 -1
  82. package/scripts/backlog/backlog-plan.ts +1 -1
  83. package/scripts/backlog/backlog-pool.ts +31 -11
  84. package/scripts/backlog/backlog-rank.ts +106 -0
  85. package/scripts/backlog/backlog-waves.ts +42 -29
  86. package/scripts/build/build-bin.ts +2 -1
  87. package/scripts/build/build-config-merge.ts +2 -1
  88. package/scripts/build/build-hooks.ts +31 -6
  89. package/scripts/build/build-managed-marker.ts +2 -1
  90. package/scripts/build/build-self-sync-guard.ts +2 -1
  91. package/scripts/build/build-version.ts +2 -1
  92. package/scripts/cases/cases-logic.ts +2 -6
  93. package/scripts/ci/pr-classification.ts +13 -11
  94. package/scripts/cost-runtime/cost-transcript.ts +4 -11
  95. package/scripts/delegation/delegation-policy.ts +11 -0
  96. package/scripts/doctor/doctor-consumer.ts +5 -12
  97. package/scripts/doctor/doctor-ports.ts +2 -2
  98. package/scripts/document/document-byte-budget.ts +13 -21
  99. package/scripts/document/document-section.ts +2 -6
  100. package/scripts/document/entry-read-budget.ts +6 -5
  101. package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
  102. package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
  103. package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
  104. package/scripts/epic/epic-audit-cli.ts +4 -4
  105. package/scripts/epic/epic-audit-orphans.ts +2 -2
  106. package/scripts/epic/epic-audit-rationale.ts +1 -1
  107. package/scripts/epic/epic-audit.ts +1 -1
  108. package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
  109. package/scripts/epic/epic-bundle-cli.ts +1 -1
  110. package/scripts/epic/epic-bundle.ts +2 -2
  111. package/scripts/epic/epic-candidate-confirm.ts +2 -1
  112. package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
  113. package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
  114. package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
  115. package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
  116. package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
  117. package/scripts/epic/epic-cross-repo.ts +21 -38
  118. package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
  119. package/scripts/epic/epic-fetch.ts +3 -3
  120. package/scripts/epic/epic-graph.ts +3 -3
  121. package/scripts/epic/epic-index.ts +2 -2
  122. package/scripts/epic/epic-issue.ts +1 -1
  123. package/scripts/epic/epic-lane-offer.ts +11 -22
  124. package/scripts/epic/epic-next.ts +3 -3
  125. package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
  126. package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
  127. package/scripts/epic/epic-rank.ts +37 -0
  128. package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
  129. package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
  130. package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
  131. package/scripts/epic/epic-relation-recheck.ts +4 -3
  132. package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
  133. package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
  134. package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
  135. package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
  136. package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
  137. package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
  138. package/scripts/epic/epic-solo.ts +29 -28
  139. package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
  140. package/{scripts-ai → scripts/epic}/epic.ts +17 -16
  141. package/scripts/gh/fix-gh-packages.ts +2 -1
  142. package/scripts/gh/gh-spawn.ts +12 -10
  143. package/scripts/git/constants.ts +1 -1
  144. package/scripts/git/git-branch.ts +10 -1
  145. package/scripts/git/git-command.ts +2 -2
  146. package/scripts/git/git-error.ts +2 -5
  147. package/scripts/git/git-execa-error.ts +6 -1
  148. package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
  149. package/scripts/git/git-followup-flush.ts +74 -48
  150. package/scripts/git/git-followup-issue-close.ts +2 -2
  151. package/scripts/git/git-followup-label.ts +1 -1
  152. package/scripts/git/git-followup-pending.ts +1 -1
  153. package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
  154. package/scripts/git/git-gh-exec.ts +85 -26
  155. package/scripts/git/git-gh-issue-read.ts +2 -2
  156. package/scripts/git/git-gh-issue-rest.ts +2 -2
  157. package/scripts/git/git-gh-issue-write.ts +6 -6
  158. package/scripts/git/git-gh-pr-read.ts +0 -21
  159. package/scripts/git/git-gh-pr.ts +2 -23
  160. package/scripts/git/git-next-issues.ts +34 -5
  161. package/scripts/git/git-pr-followup-wrapup.ts +2 -6
  162. package/scripts/git/git-preflight.ts +25 -10
  163. package/scripts/git/git-push-transport.ts +12 -69
  164. package/scripts/git/git-spawn.ts +79 -3
  165. package/scripts/git/git-ssh-keepalive.ts +74 -0
  166. package/scripts/git/git-staging.ts +21 -9
  167. package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
  168. package/scripts/git/github-issue-url.ts +1 -1
  169. package/scripts/git/gone-branch.ts +3 -2
  170. package/scripts/git/issue-labels.ts +13 -1
  171. package/scripts/git/issue-rank.ts +89 -0
  172. package/scripts/git/issue-reference.ts +19 -0
  173. package/scripts/git/main-merge.ts +2 -1
  174. package/scripts/git/main-sync.ts +3 -2
  175. package/scripts/git/schemas.ts +1 -1
  176. package/scripts/git/stash-pop-args.ts +7 -13
  177. package/scripts/git/stash-pop-cli.ts +2 -1
  178. package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
  179. package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
  180. package/scripts/hooks/format-edited-cli.ts +15 -0
  181. package/scripts/hooks/format-edited-file.ts +2 -9
  182. package/scripts/hooks/format-rewrite-notice.ts +2 -8
  183. package/scripts/hooks/pretool-guard-cli.ts +21 -0
  184. package/scripts/hooks/pretool-guard.ts +14 -15
  185. package/scripts/init/init-ai-copy.ts +2 -5
  186. package/scripts/init/init.ts +8 -4
  187. package/scripts/init/start-exec.ts +7 -1
  188. package/scripts/init/start-plan.ts +18 -5
  189. package/scripts/init/start-setup-pr.ts +46 -19
  190. package/scripts/init/start-steps.ts +43 -10
  191. package/scripts/init/start.ts +42 -11
  192. package/scripts/issue/issue-cite.ts +19 -1
  193. package/scripts/issue/issue-comment-cli.ts +2 -1
  194. package/scripts/issue/issue-file-cli.ts +2 -1
  195. package/scripts/issue/issue-fold-cli.ts +9 -16
  196. package/scripts/issue/issue-scout-cli.ts +8 -13
  197. package/scripts/issue/measure-rerun-cli.ts +7 -6
  198. package/scripts/josh/josh-command-map.ts +0 -102
  199. package/scripts/josh/josh-commands-ai.ts +2 -2
  200. package/scripts/josh/josh-commands-backlog.ts +1 -1
  201. package/scripts/josh/josh-commands-development.ts +3 -3
  202. package/scripts/josh/josh-commands-project.ts +1 -1
  203. package/scripts/josh/josh-commands-workflow.ts +4 -4
  204. package/scripts/lane/lane-child-marker.ts +16 -2
  205. package/scripts/lane/lane-cli.ts +34 -15
  206. package/scripts/lane/lane-open.ts +3 -2
  207. package/scripts/lane/lane-registry.ts +3 -6
  208. package/scripts/lane/lane-report.ts +10 -4
  209. package/scripts/lib/cli-flags.ts +20 -2
  210. package/scripts/lib/error-message.ts +9 -0
  211. package/scripts/lib/read-file.ts +26 -2
  212. package/scripts/observations/observation-ledger-home.ts +98 -26
  213. package/scripts/observations/observation-ledger-migrate.ts +26 -21
  214. package/scripts/observations/observation-ledger-prepare.ts +10 -20
  215. package/scripts/observations/observation-ledger.ts +58 -25
  216. package/scripts/observations/observations-flush-cli.ts +6 -23
  217. package/scripts/observations/observations-flush-landing.ts +2 -1
  218. package/scripts/observations/observations-flush.ts +29 -20
  219. package/scripts/package/package-scout-cli.ts +8 -16
  220. package/scripts/ports/port-command.ts +2 -1
  221. package/scripts/propagate/propagate-plan.ts +185 -0
  222. package/scripts/propagate/propagate-publish.ts +6 -7
  223. package/scripts/propagate/propagate-run.ts +5 -1
  224. package/scripts/propagate/propagate-steps.ts +18 -2
  225. package/scripts/propagate/propagate-targets.ts +13 -2
  226. package/scripts/propagate/propagate.ts +63 -44
  227. package/scripts/refactor/import-graph.ts +2 -6
  228. package/scripts/registry-migration/migrate-logic.ts +1 -0
  229. package/scripts/registry-migration/migrate.ts +3 -2
  230. package/scripts/registry-migration/user-npmrc.ts +2 -2
  231. package/scripts/release/github-release.ts +1 -1
  232. package/scripts/release/release-cli.ts +2 -1
  233. package/scripts/repo/repo-setting.ts +10 -8
  234. package/scripts/review/review-attest.ts +44 -0
  235. package/scripts/review/review-brief-cli.ts +49 -2
  236. package/scripts/review/review-brief.ts +46 -1
  237. package/scripts/review/review-finding-ledger.ts +3 -3
  238. package/scripts/review/review-findings-cli.ts +4 -11
  239. package/scripts/review/review-record-cli.ts +14 -11
  240. package/scripts/review/review-record.ts +6 -16
  241. package/scripts/review/review-stamps.ts +7 -0
  242. package/scripts/rules/decision-oracle.ts +2 -2
  243. package/scripts/rules/implementation-cut.ts +1 -1
  244. package/scripts/rules/issue-fold.ts +1 -2
  245. package/scripts/rules/lane-carry-conflict.ts +1 -2
  246. package/scripts/rules/lane-park.ts +1 -2
  247. package/scripts/rules/pre-gate-cut.ts +2 -2
  248. package/scripts/rules/rule-body-guard.ts +2 -2
  249. package/scripts/rules/rule-value-cache.ts +99 -0
  250. package/scripts/rules/rule-value-cli.ts +10 -9
  251. package/scripts/rules/rule-value.ts +1 -1
  252. package/scripts/run/run-carry-args.ts +2 -6
  253. package/scripts/run/run-carry-cli.ts +5 -7
  254. package/scripts/run/run-cut-args.ts +7 -13
  255. package/scripts/run/run-ending-cli.ts +4 -7
  256. package/scripts/run/run-event-stream.ts +2 -5
  257. package/scripts/run/run-headless.ts +22 -6
  258. package/scripts/run/run-liveness-cli.ts +4 -7
  259. package/scripts/run/run-parent-cut-hook.ts +144 -0
  260. package/scripts/run/run-progress-args.ts +2 -6
  261. package/scripts/run/run-progress-cli.ts +2 -1
  262. package/scripts/run/run-progress-read.ts +2 -1
  263. package/scripts/run/run-progress.ts +21 -8
  264. package/scripts/run/run-ship-cli.ts +6 -74
  265. package/scripts/run/run-ship-preflight.ts +87 -0
  266. package/scripts/run/run-ship-review-steps.ts +5 -26
  267. package/scripts/run/run-ship-review.ts +9 -0
  268. package/scripts/run/run-ship-scoped.ts +44 -0
  269. package/scripts/run/run-ship-stage.ts +5 -1
  270. package/scripts/run/run-ship-steps.ts +105 -0
  271. package/scripts/run/run-ship.ts +2 -0
  272. package/scripts/run/run-tail-cli.ts +3 -3
  273. package/scripts/run/run-tidy-stashes.ts +7 -14
  274. package/scripts/run/run-wake-cli.ts +4 -8
  275. package/scripts/run/run-wake-describe.ts +19 -1
  276. package/scripts/run/run-wake-driver.ts +15 -2
  277. package/scripts/run/run-wake-session.ts +14 -4
  278. package/scripts/run/run-watcher-hook.ts +1 -1
  279. package/scripts/safe-chain/project-config.ts +3 -6
  280. package/scripts/security/security-audit-provision.ts +6 -7
  281. package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
  282. package/scripts/sync/directory-copy-guard.ts +2 -1
  283. package/scripts/sync/sync-ai-files.ts +3 -3
  284. package/scripts/sync/template-source-logic.ts +3 -4
  285. package/scripts/test/e2e-retry-check.ts +3 -6
  286. package/scripts/time-runtime/time-bundle-call.ts +2 -2
  287. package/scripts/ui/ui-routes-cli.ts +3 -6
  288. package/scripts/version/bump-version.ts +2 -1
  289. package/scripts/version/npm-registry.ts +97 -0
  290. package/scripts/version/version-commands.ts +5 -4
  291. package/scripts/version/version-remote.ts +38 -33
  292. package/scripts/version/version-targets.ts +3 -3
  293. package/.claude/skills/workflow-commands/rule-residency.md +0 -267
  294. package/evals/scenarios/consult-not-execute.json +0 -20
  295. package/evals/scenarios/no-implicit-workflow.json +0 -26
  296. package/evals/scenarios/overrides-both-locations.json +0 -29
  297. package/evals/scenarios/upstream-interrupt.json +0 -23
  298. package/evals/scenarios/workflow-skill-loaded.json +0 -16
  299. package/prompts/agent-lint-protocol.json +0 -79
  300. package/prompts/agent-rules.md +0 -63
  301. package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
  302. package/prompts/collaboration-workflow/distributed-docs.md +0 -7
  303. package/prompts/collaboration-workflow/durable-rules.md +0 -24
  304. package/prompts/collaboration-workflow/latest-first.md +0 -14
  305. package/prompts/collaboration-workflow/no-clones.md +0 -11
  306. package/prompts/collaboration-workflow/simplicity-first.md +0 -9
  307. package/prompts/collaboration-workflow/single-source-rules.md +0 -13
  308. package/scripts/build/shim-shell.ts +0 -25
  309. package/scripts/claude/skill-meta.ts +0 -37
  310. package/scripts/document/document-scan.ts +0 -108
  311. package/scripts/document/generate-catalog.ts +0 -69
  312. package/scripts/document/retired-phrases.ts +0 -143
  313. package/scripts/document/review-agent-type.ts +0 -46
  314. package/scripts/document/single-source.ts +0 -73
  315. package/scripts/git/git-fixture-workspace.ts +0 -88
  316. package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
  317. package/scripts/hooks/prompt-hooks.ts +0 -38
  318. package/scripts/init/hook-command-bootstrap.ts +0 -83
  319. package/scripts/issue/issue-logic.ts +0 -50
  320. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
  321. package/scripts/josh/josh-script-reader.ts +0 -11
  322. package/scripts/package/production-dependency-graph.ts +0 -68
  323. package/scripts/retrospective/retrospective-cli.ts +0 -97
  324. package/scripts/retrospective/retrospective.ts +0 -127
  325. package/scripts/rules/delivered-rules-harness.ts +0 -72
  326. package/scripts/rules/top-level-scripts-guard.ts +0 -47
  327. package/scripts/run/run-carry-flush.ts +0 -58
  328. package/scripts/run/run-transcript.golden.txt +0 -57
  329. package/scripts/run/run-wake-work.ts +0 -108
  330. package/scripts/sync/dependabot-pin-logic.ts +0 -135
  331. package/scripts/test/classify-isolation.ts +0 -149
  332. package/scripts/test/http-fault-injection.ts +0 -79
  333. package/scripts/test/josh-harness-environment.ts +0 -260
  334. package/scripts/test/josh-harness.ts +0 -96
  335. package/scripts/test/pilot-files.ts +0 -452
  336. package/scripts/test/pilot-measure.ts +0 -78
  337. package/scripts/test/test-network-guard.ts +0 -453
  338. package/scripts/test/test-repository-guard.ts +0 -317
  339. package/scripts/test/test-state-guard.ts +0 -85
  340. package/scripts/test/test-stdout-guard.ts +0 -54
  341. package/scripts/test/test-telegram-guard.ts +0 -90
  342. package/scripts/test/unit-guard-environment.ts +0 -12
  343. package/scripts/test/unit-projects.ts +0 -139
  344. package/scripts/test/vitest-include-globs.ts +0 -12
  345. package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
  346. package/scripts-ai/completion-gate.ts +0 -13
  347. package/scripts-ai/environment-loader.ts +0 -9
  348. /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
  349. /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
  350. /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
@@ -22,6 +22,11 @@ function has_timed_out(error: unknown): boolean {
22
22
  return error.timedOut === true
23
23
  }
24
24
 
25
+ // A timeout has no exit code — the process was killed rather than allowed to exit — so `cause` says
26
+ // so in the slot every other git failure puts a number in. Shared by every git spawn that carries a
27
+ // budget: the push (`git-push-transport.ts`) and the fetch and pull (`git-spawn.ts`).
28
+ const TIMEOUT_EXIT_CODE = 'timeout'
29
+
25
30
  // The error a spawned git command fails with. The exit code is carried on `cause` as well as in the
26
31
  // message, because callers branch on it rather than parse it: `git_command.push` reads 128 there to
27
32
  // decide whether to retry with `--set-upstream`.
@@ -32,4 +37,4 @@ function create_spawn_error(command: string, exit_code: number | undefined): Err
32
37
  return new Error(error_message, { cause: { exit_code: exit_code_string } })
33
38
  }
34
39
 
35
- export { create_spawn_error, get_exit_code, has_timed_out }
40
+ export { create_spawn_error, get_exit_code, has_timed_out, TIMEOUT_EXIT_CODE }
@@ -1,11 +1,11 @@
1
- import { git_followup_cleanup, type CleanupStep } from '../scripts/git/git-followup-cleanup'
2
- import { git_followup_pending } from '../scripts/git/git-followup-pending'
3
- import { git_next_issues } from '../scripts/git/git-next-issues'
4
- import { review_attest } from '../scripts/review/review-attest'
5
- import { review_stamps } from '../scripts/review/review-stamps'
6
- import { run_hold } from '../scripts/run/run-hold'
7
- import { run_progress_clock } from '../scripts/run/run-progress-clock'
1
+ import { review_attest } from '#scripts/review/review-attest'
2
+ import { review_stamps } from '#scripts/review/review-stamps'
3
+ import { run_hold } from '#scripts/run/run-hold'
4
+ import { run_progress_clock } from '#scripts/run/run-progress-clock'
8
5
  import { parse_completed_issue_number } from './followup-issue-number'
6
+ import { git_followup_cleanup, type CleanupStep } from './git-followup-cleanup'
7
+ import { git_followup_pending } from './git-followup-pending'
8
+ import { git_next_issues } from './git-next-issues'
9
9
 
10
10
  // **The count of unreleased merges, not the project version** (joshuafolkken/kit#1486). This line
11
11
  // used to read the local `package.json` and was read as "the version this run just shipped"; children
@@ -1,62 +1,88 @@
1
1
  import { observation_ledger } from '#scripts/observations/observation-ledger'
2
- import { observation_ledger_home } from '#scripts/observations/observation-ledger-home'
2
+ import { observation_ledger_prepare } from '#scripts/observations/observation-ledger-prepare'
3
3
  import { observations_flush } from '#scripts/observations/observations-flush'
4
4
  import { git_command } from './git-command'
5
- import { git_followup_cleanup } from './git-followup-cleanup'
6
- import { main_sync } from './main-sync'
5
+ import { git_spawn } from './git-spawn'
7
6
 
8
7
  // The residual commit path for the observation ledger (joshuafolkken/kit#1810). A run's appended lines
9
- // ride its own commit (joshuafolkken/kit#2763), so after the merge the ledger is usually clean and this
10
- // short-circuits; what it still catches is a line appended after the commit — a second review round's
11
- // record — which would otherwise stay in the working tree until a person remembered to flush it. This
12
- // wires the flush into `pnpm josh followup`, the one step that knows the merge has landed.
8
+ // ride its own commit (joshuafolkken/kit#2763), so the ledger is usually clean by now and this
9
+ // short-circuits; what it still catches is a line appended after that commit — a second review round's
10
+ // record — which would otherwise stay in the working tree after the merge.
13
11
  //
14
- // **It runs before the working-tree hold is released** (`scripts-ai/git-followup-finish.ts`, whose
15
- // `finish()` runs after `git_pr_followup.run()` returns), because a flush switches branches, and a
16
- // hold released first would let another run grab the tree mid-switch.
17
-
18
- const MAIN_SYNC_SUCCESS = 0
19
- const FLUSH_RECOVERY = 'pnpm josh observations:flush'
20
- const SYNC_FAILURE_MESSAGE =
21
- 'Could not return to the default branch to flush the observation ledger; `pnpm josh ms` printed the reason above.'
22
-
23
- // `observations_flush.flush` requires the default branch, so the checkout is returned to it first.
24
- // `main_sync.run` catches its own errors and answers with an exit code rather than throwing, so a
25
- // checkout it could not move is turned into the throw the guarded step below reports.
26
- async function sync_and_flush(): Promise<void> {
27
- if ((await main_sync.run([])) !== MAIN_SYNC_SUCCESS) throw new Error(SYNC_FAILURE_MESSAGE)
28
-
29
- console.info(await observations_flush.flush(new Date()))
12
+ // **It commits onto the pull request's own branch, before the merge** (joshuafolkken/kit#2919). It used
13
+ // to flush after the merge from the default branch, which a lane cannot reach, so a lane's lines
14
+ // waited in the primary checkout for a batch-end flush and were lost there to another run's stash.
15
+ // Committed to the branch instead, the lines merge with the pull request — in a lane as anywhere — and
16
+ // the CI wait `pnpm josh followup` runs next covers the pushed commit like any other push.
17
+
18
+ const COMMIT_MESSAGE_PREFIX = 'Record observation ledger entries'
19
+
20
+ function commit_message(issue_number: string | undefined): string {
21
+ return issue_number === undefined
22
+ ? COMMIT_MESSAGE_PREFIX
23
+ : `${COMMIT_MESSAGE_PREFIX} #${issue_number}`
30
24
  }
31
25
 
32
- // **Short-circuits when the ledger holds no pending append.** That is every run that recorded no
33
- // observation, so the ordinary run pays nothing. Guarded like its sibling tail steps (joshuafolkken/kit#1539): past the merge a flush
34
- // failure is reported, never allowed to take the merge, the epic close and the hold release with it.
35
- //
36
- // **A lane skips it outright** (joshuafolkken/kit#2419). Its ledger is the primary checkout's
37
- // (`observation-ledger-home.ts`), which this step cannot flush — `main_sync` refuses in a linked work
38
- // tree — so the line waits in the primary checkout and the `backlogrun` commits every lane's lines at
39
- // once through `pnpm josh run:carry --end` (joshuafolkken/kit#2492).
40
- async function flush_ledger_step(should_merge: boolean): Promise<void> {
41
- if (
42
- !should_merge ||
43
- observation_ledger_home.is_lane() ||
44
- !observation_ledger.has_pending_append(await git_command.status())
45
- ) {
46
- return
26
+ // **A broken line refuses the merge rather than riding it** (joshuafolkken/kit#2123): the branch's
27
+ // ledger file goes away with the lane, so a line left behind here is a line lost.
28
+ async function assert_grammar(): Promise<void> {
29
+ const broken = await observation_ledger_prepare.prepare(await git_command.repository_root())
30
+
31
+ if (broken.length > 0) throw new Error(observations_flush.broken_lines_message(broken))
32
+ }
33
+
34
+ // **A push that fails takes its commit back**, leaving the lines staged: the next `pnpm josh followup`
35
+ // then finds them pending and retries. Left committed, they would read as clean and the merge would
36
+ // go ahead without them, the lines stranded in a local commit no reader sees.
37
+ async function push_or_undo_commit(): Promise<void> {
38
+ try {
39
+ await git_command.push()
40
+ } catch (error) {
41
+ await git_spawn.read(['reset', '--soft', 'HEAD~1'])
42
+ throw error
47
43
  }
44
+ }
45
+
46
+ // Only the ledger paths are staged — whatever else the tree holds is the run's to commit, not this
47
+ // step's — and `-A` takes a migration's deletion of an old path with it.
48
+ async function commit_and_push(
49
+ paths: ReadonlyArray<string>,
50
+ issue_number: string | undefined,
51
+ ): Promise<void> {
52
+ await git_spawn.read(['add', '-A', '--', ...paths])
53
+ await git_command.commit(commit_message(issue_number))
54
+ await push_or_undo_commit()
55
+ }
56
+
57
+ // **Short-circuits when the ledger holds no pending append**, which is every run that recorded nothing
58
+ // after its commit, and on a `--no-merge` run, which has no merge for the lines to ride.
59
+ async function commit_ledger_step(
60
+ should_merge: boolean,
61
+ issue_number: string | undefined,
62
+ ): Promise<void> {
63
+ if (!should_merge) return
64
+
65
+ const status_output = await git_command.status()
66
+
67
+ if (!observation_ledger.has_pending_append(status_output)) return
68
+
69
+ await assert_grammar()
70
+
71
+ // Read again: the grammar check migrated any old-path ledger, which changes what is to be staged.
72
+ const paths = observation_ledger
73
+ .ledger_paths(await git_command.status())
74
+ .filter((file_path) => !observation_ledger.is_migration_claim(file_path))
75
+
76
+ // A live run's migration claim alone is pending, not a line: with no path, `git add -A --` would
77
+ // stage the whole tree.
78
+ if (paths.length === 0) return
48
79
 
49
- // `should_merge` is always true past the early return; it is passed on as `run_guarded_step`'s
50
- // guard flag rather than a literal `true` so this step stays identical to its sibling tail steps —
51
- // hardcoding `true` would turn the guard on for a `--no-merge` run if the early return were ever
52
- // dropped (the trap `in_progress_step` warns of).
53
- await git_followup_cleanup.run_guarded_step(should_merge, {
54
- label: 'The observation ledger flush',
55
- recovery: FLUSH_RECOVERY,
56
- run: sync_and_flush,
57
- })
80
+ await commit_and_push(paths, issue_number)
81
+ console.info(
82
+ `💡 Committed ${String(paths.length)} observation ledger path(s) to the pull request.`,
83
+ )
58
84
  }
59
85
 
60
- const git_followup_flush = { flush_ledger_step }
86
+ const git_followup_flush = { COMMIT_MESSAGE_PREFIX, commit_ledger_step }
61
87
 
62
88
  export { git_followup_flush }
@@ -1,6 +1,6 @@
1
+ import { epic_parse } from '#scripts/epic/epic-parse'
1
2
  import { poll, type PollOptions } from '#scripts/lib/poll'
2
3
  import { z } from 'zod'
3
- import { git_epic_parse } from './git-epic-parse'
4
4
  import { git_gh_command } from './git-gh-command'
5
5
  import { parse_json_object_safe } from './parse-json-array'
6
6
 
@@ -28,7 +28,7 @@ async function is_issue_closed(issue_number: string): Promise<boolean> {
28
28
  const raw = await git_gh_command.issue_get_state_and_relations(issue_number)
29
29
  const parsed = raw === undefined ? undefined : parse_json_object_safe(raw, state_read_schema)
30
30
 
31
- return git_epic_parse.is_state_closed(parsed?.state)
31
+ return epic_parse.is_state_closed(parsed?.state)
32
32
  }
33
33
 
34
34
  const FOLLOWUP_CLOSER = 'pnpm josh followup'
@@ -46,7 +46,7 @@ async function read_label_names(issue_number: string): Promise<ReadonlyArray<str
46
46
  // **An issue that does not carry it is never written to**, which is what keeps the step silent on a
47
47
  // run whose issue was never labelled — a `404` swallowed into a warning would report a failure where
48
48
  // there was nothing to do. The read itself is always paid: one `number,labels,body` call per merged
49
- // run, which is the read `git-epic-read.ts` already makes rather than a field list of its own.
49
+ // run, which is the read `epic-read.ts` already makes rather than a field list of its own.
50
50
  //
51
51
  // A run whose pull request named no issue has nothing to strip, so it returns rather than throwing:
52
52
  // the number is recovered from the `closes #N` keyword, and its absence is already reported where
@@ -21,7 +21,7 @@ import { git_command } from './git-command'
21
21
  // there cannot include this run's own merge, and a bare number would be understated by exactly one.
22
22
  // Saying so is the honest form of the same measurement. The console line printed after the merge
23
23
  // passes `false` and carries no note, because by then the merge is on the remote and the fetch below
24
- // brings it in; `scripts-ai/git-followup-finish.ts` is that caller, so **both branches of this flag
24
+ // brings it in; `git-followup-finish.ts` is that caller, so **both branches of this flag
25
25
  // are live**. What a run *does* about the count is `pnpm josh release:scope`, asked after the merge,
26
26
  // which reads `read_pending` below rather than this line (joshuafolkken/kit#1582).
27
27
  const MERGE_PENDING_NOTE = "— this run's merge is not counted; it lands next"
@@ -1,18 +1,19 @@
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_error } from '../scripts/git/git-error'
5
- import { git_notify, type GitNotifyConfig } from '../scripts/git/git-notify'
6
- import { git_pr_followup } from '../scripts/git/git-pr-followup'
7
- import { cli_body } from '../scripts/josh/cli-body'
8
- import { live_evidence } from '../scripts/review/live-evidence'
9
- import { review_attest } from '../scripts/review/review-attest'
10
- import { review_record } from '../scripts/review/review-record'
11
- import { load_optional_environment } from './environment-loader'
3
+ import { cli_body } from '#scripts/josh/cli-body'
4
+ import { josh_environment_file } from '#scripts/josh/josh-environment-file'
5
+ import { live_evidence } from '#scripts/review/live-evidence'
6
+ import { review_attest } from '#scripts/review/review-attest'
7
+ import { review_record } from '#scripts/review/review-record'
12
8
  import { parse_issue_number_from_text } from './followup-issue-number'
9
+ import { git_branch } from './git-branch'
10
+ import { git_error } from './git-error'
13
11
  import { git_followup_finish } from './git-followup-finish'
12
+ import { git_followup_flush } from './git-followup-flush'
13
+ import { git_notify, type GitNotifyConfig } from './git-notify'
14
+ import { git_pr_followup } from './git-pr-followup'
14
15
 
15
- load_optional_environment()
16
+ josh_environment_file.load_environment_file()
16
17
 
17
18
  // cspell:words coderabbit
18
19
 
@@ -202,6 +203,17 @@ async function assert_merge_gates(
202
203
  await assert_live_evidence(should_merge, branch_name)
203
204
  }
204
205
 
206
+ // After the gates, before the CI wait: a line appended since the run's commit rides the pull request
207
+ // itself, so it merges with it — a lane's included (joshuafolkken/kit#2919).
208
+ async function prepare_merge(
209
+ should_merge: boolean,
210
+ issue_number: string | undefined,
211
+ branch_name: string,
212
+ ): Promise<void> {
213
+ await assert_merge_gates(should_merge, issue_number, branch_name)
214
+ await git_followup_flush.commit_ledger_step(should_merge, issue_number)
215
+ }
216
+
205
217
  async function main(): Promise<void> {
206
218
  const cli = parse_cli_arguments()
207
219
 
@@ -216,7 +228,7 @@ async function main(): Promise<void> {
216
228
  const should_merge = is_merge_resolved(cli.values)
217
229
  const branch_name = await resolve_branch_name(cli.values.branch)
218
230
 
219
- await assert_merge_gates(should_merge, issue_number, branch_name)
231
+ await prepare_merge(should_merge, issue_number, branch_name)
220
232
  // **The number the run reports on is the one it used**, which is the number the pull request
221
233
  // closes where the invocation named none (joshuafolkken/kit#1539). Recovered inside `run`, so the
222
234
  // tail records a run the command line could not identify rather than silently skipping it.
@@ -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
  }