@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
@@ -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,12 +1,14 @@
1
1
  import { git_gh_command } from './git-gh-command'
2
2
  import { has_any_label, NOT_DIRECTLY_RUNNABLE_LABELS } from './issue-labels'
3
+ import { issue_rank, type DependencyRow } from './issue-rank'
3
4
  import { parse_json_array_safe } from './parse-json-array'
4
5
  import { open_issue_schema, type OpenIssueData } from './schemas'
5
6
 
6
7
  // The next-issues display printed when a workflow completes (#821): up to five open issues in
7
8
  // priority order, so the user picks the next run from the completion output instead of opening the
8
9
  // issue list. Priority is recency — a newer issue usually encodes the most current understanding
9
- // of the backlog — with the label-based exclusions below.
10
+ // of the backlog — with the label-based exclusions below, under the ranking keys `issue-rank.ts`
11
+ // defines (joshuafolkken/kit#2928).
10
12
  //
11
13
  // `prioritize` is also the order `epicrun` picks up `auto-ok` issues in (joshuafolkken/kit#906).
12
14
  // The two ask one question — which open issue outside an epic is run next — so they share one
@@ -37,6 +39,34 @@ function compare_newest_first(first: OpenIssueData, second: OpenIssueData): numb
37
39
  return first.createdAt < second.createdAt ? 1 : -1
38
40
  }
39
41
 
42
+ // A row's blockers by number. **Lossy across repositories**: a blocker elsewhere sharing a number
43
+ // with a row here counts as one of its dependents. The listings this ranks are one repository's own,
44
+ // and `backlog:next` re-ranks its offer from the qualified graph (`backlog-rank.ts`), so the cost is a
45
+ // display row placed one slot off rather than a run started early.
46
+ function dependency_row(issue: OpenIssueData): DependencyRow {
47
+ return {
48
+ key: String(issue.number),
49
+ blockers: (issue.blockedBy?.nodes ?? []).map((blocker) => String(blocker.number)),
50
+ }
51
+ }
52
+
53
+ // Every candidate in rank order, uncapped. `backlog:next` takes this rather than `prioritize`, so the
54
+ // `run:solo` gate sees the whole ranking before anything is cut (joshuafolkken/kit#2928).
55
+ function order(
56
+ issues: ReadonlyArray<OpenIssueData>,
57
+ completed_issue_number?: number,
58
+ ): Array<OpenIssueData> {
59
+ const candidates = issues
60
+ .filter((issue) => is_candidate(issue, completed_issue_number))
61
+ .toSorted(compare_newest_first)
62
+ const dependents = issue_rank.count_dependents(candidates.map((issue) => dependency_row(issue)))
63
+
64
+ return issue_rank.rank(candidates, (issue) => ({
65
+ labels: (issue.labels ?? []).map((label) => label.name),
66
+ dependents: dependents.get(String(issue.number)) ?? 0,
67
+ }))
68
+ }
69
+
40
70
  // **This display deliberately does not drop a blocked issue, and the `auto-ok` pickup deliberately
41
71
  // does.** They share this ordering and differ on the set, which reads as an inconsistency and is not
42
72
  // one (joshuafolkken/kit#1005).
@@ -50,10 +80,7 @@ function prioritize(
50
80
  issues: ReadonlyArray<OpenIssueData>,
51
81
  completed_issue_number?: number,
52
82
  ): Array<OpenIssueData> {
53
- return issues
54
- .filter((issue) => is_candidate(issue, completed_issue_number))
55
- .toSorted(compare_newest_first)
56
- .slice(0, DISPLAY_LIMIT)
83
+ return order(issues, completed_issue_number).slice(0, DISPLAY_LIMIT)
57
84
  }
58
85
 
59
86
  // Empty input yields no lines at all — a bare header with nothing under it reads as an error.
@@ -90,6 +117,8 @@ async function fetch_next_issue_lines(completed_issue_number?: number): Promise<
90
117
  }
91
118
 
92
119
  const git_next_issues = {
120
+ DISPLAY_LIMIT,
121
+ order,
93
122
  prioritize,
94
123
  format_lines,
95
124
  fetch_next_issue_lines,
@@ -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
 
@@ -120,19 +117,37 @@ function refusal(problems: ReadonlyArray<string>): Error {
120
117
  )
121
118
  }
122
119
 
123
- async function check(input: PreflightInput): Promise<void> {
120
+ interface PreflightAnswer {
121
+ pr_target: Target | undefined
122
+ problems: Array<string>
123
+ }
124
+
125
+ async function answer(input: PreflightInput): Promise<PreflightAnswer> {
124
126
  const { target, problems } = await resolve_target(input.cli_input)
125
127
  // The classification and the evidence belong to the pull request, so a `--skip-pr` run asks neither.
126
128
  const pr_target = input.will_open_pr ? target : undefined
127
129
  const pr_problems = pr_target === undefined ? [] : await classification_problems(pr_target)
128
- const all_problems = [...problems, ...pr_problems]
129
130
 
130
- if (all_problems.length > 0) throw refusal(all_problems)
131
+ return { pr_target, problems: [...problems, ...pr_problems] }
132
+ }
133
+
134
+ // joshuafolkken/kit#2946: the unmet preconditions as a list, so `josh ship` can ask them before its
135
+ // review and gate start instead of meeting the refusal at the commit stage.
136
+ async function problems_of(input: PreflightInput): Promise<Array<string>> {
137
+ const { problems } = await answer(input)
138
+
139
+ return problems
140
+ }
141
+
142
+ async function check(input: PreflightInput): Promise<void> {
143
+ const { pr_target, problems } = await answer(input)
144
+
145
+ if (problems.length > 0) throw refusal(problems)
131
146
 
132
147
  if (pr_target !== undefined) await announce_evidence(pr_target.branch_name)
133
148
  }
134
149
 
135
- const git_preflight = { check }
150
+ const git_preflight = { check, problems_of }
136
151
 
137
152
  export type { PreflightInput }
138
153
  export { git_preflight }
@@ -1,6 +1,12 @@
1
1
  import { execa } from 'execa'
2
2
  import { git_utilities } from './constants'
3
- import { create_spawn_error, get_exit_code, has_timed_out } from './git-execa-error'
3
+ import {
4
+ create_spawn_error,
5
+ get_exit_code,
6
+ has_timed_out,
7
+ TIMEOUT_EXIT_CODE,
8
+ } from './git-execa-error'
9
+ import { git_ssh_keepalive } from './git-ssh-keepalive'
4
10
 
5
11
  const MS_PER_SECOND = 1000
6
12
 
@@ -33,65 +39,10 @@ const PUSH_TIMEOUT_LABEL = `${String(PUSH_TIMEOUT_SECONDS)}s`
33
39
  // said no" are different diagnoses, and only the first one is worth retrying.
34
40
  const PUSH_TIMEOUT_MESSAGE = 'git push timed out'
35
41
 
36
- // A timeout has no exit code — the process was killed rather than allowed to exit — so `cause` says
37
- // so in the slot every other git failure puts a number in. `is_upstream_not_set_error` compares
38
- // against '128', which this can never equal, so a timed-out bare push is never mistaken for a
39
- // missing upstream and retried as `--set-upstream`.
40
- const PUSH_TIMEOUT_EXIT_CODE = 'timeout'
41
-
42
- // SSH's own liveness probe: three unanswered checks 15 seconds apart, so a connection that dies
43
- // mid-transfer is noticed in about 45 seconds instead of waiting on the TCP default. The budget
44
- // above bounds the damage either way; this is what makes the common case fail in seconds rather
45
- // than sit out the whole budget.
46
- const SSH_KEEPALIVE_OPTIONS = '-o ServerAliveInterval=15 -o ServerAliveCountMax=3'
47
- const SSH_COMMAND_VARIABLE = 'GIT_SSH_COMMAND'
48
- const SSH_LEGACY_VARIABLE = 'GIT_SSH'
49
- const KEEPALIVE_SSH_COMMAND = `ssh ${SSH_KEEPALIVE_OPTIONS}`
50
- const SSH_COMMAND_CONFIG_KEY = 'core.sshCommand'
42
+ // `TIMEOUT_EXIT_CODE` is never '128', which `is_upstream_not_set_error` compares against, so a
43
+ // timed-out bare push is never mistaken for a missing upstream and retried as `--set-upstream`.
51
44
  const PUSH_SUBCOMMAND = 'push'
52
45
 
53
- // Whatever already decides how ssh is invoked, or an empty string when nothing does. git reads three
54
- // sources in this order — `GIT_SSH_COMMAND`, `core.sshCommand`, then the legacy `GIT_SSH` — and the
55
- // last one has to be asked as well precisely because it loses: setting `GIT_SSH_COMMAND` on top of a
56
- // `GIT_SSH=plink.exe` outranks it, so the push would run an ssh binary the user did not choose.
57
- function to_environment_value(name: string): string {
58
- return process.env[name]?.trim() ?? ''
59
- }
60
-
61
- function to_configured_ssh_command(): string {
62
- const from_command = to_environment_value(SSH_COMMAND_VARIABLE)
63
-
64
- return from_command === '' ? to_environment_value(SSH_LEGACY_VARIABLE) : from_command
65
- }
66
-
67
- // Whether any of those three sources answered. **A hit means hands off entirely.**
68
- //
69
- // Appending the options to a command someone else chose is not safe, because they are OpenSSH's:
70
- // `plink` and `TortoiseGitPlink` are supported ssh commands on the platform `get_git_command_for_spawn`
71
- // goes out of its way to handle, and a wrapper script with a fixed argument list is common on any
72
- // platform — each of them exits on a usage error rather than pushing. Setting the variable while
73
- // `core.sshCommand` is configured is the same mistake from the other side: the environment wins, so
74
- // a per-repository key would be silently replaced by a plain `ssh`.
75
- //
76
- // What those users lose is the keepalive, not the fix: the budget above still bounds their push, and
77
- // `ServerAliveInterval` belongs in their own ssh config where it applies to every tool they run.
78
- async function has_configured_ssh_command(): Promise<boolean> {
79
- if (to_configured_ssh_command() !== '') return true
80
-
81
- try {
82
- const git_command_bin = git_utilities.get_git_command_for_spawn()
83
- // execa runs the binary directly with an argument array and no `shell` option, so CLI
84
- // args cannot break out of a shell sandbox; the git command and args are internally
85
- // controlled, never untrusted input. tssecurity:S8705 is a false positive here.
86
- const { stdout } = await execa(git_command_bin, ['config', '--get', SSH_COMMAND_CONFIG_KEY]) // NOSONAR
87
-
88
- return stdout.trim() !== ''
89
- } catch {
90
- // `git config --get` exits 1 when the key is unset, which is the answer rather than a failure.
91
- return false
92
- }
93
- }
94
-
95
46
  interface PushOptions {
96
47
  stdio: 'inherit'
97
48
  timeout: number
@@ -99,12 +50,10 @@ interface PushOptions {
99
50
  }
100
51
 
101
52
  async function to_push_options(): Promise<PushOptions> {
102
- const should_keep_alive = !(await has_configured_ssh_command())
103
-
104
53
  return {
105
54
  stdio: 'inherit',
106
55
  timeout: PUSH_TIMEOUT_MS,
107
- env: should_keep_alive ? { [SSH_COMMAND_VARIABLE]: KEEPALIVE_SSH_COMMAND } : {},
56
+ env: await git_ssh_keepalive.to_environment(),
108
57
  }
109
58
  }
110
59
 
@@ -142,7 +91,7 @@ function to_retry_notice(arguments_list: Array<string>): string {
142
91
  function create_timeout_error(arguments_list: Array<string>): Error {
143
92
  const message = `${PUSH_TIMEOUT_MESSAGE} twice (${PUSH_TIMEOUT_LABEL} each). Re-run it by hand: ${to_manual_command(arguments_list)}`
144
93
 
145
- return new Error(message, { cause: { exit_code: PUSH_TIMEOUT_EXIT_CODE } })
94
+ return new Error(message, { cause: { exit_code: TIMEOUT_EXIT_CODE } })
146
95
  }
147
96
 
148
97
  // One push, bounded and retried once — and **only** on a timeout. A push the remote rejected (a
@@ -164,10 +113,4 @@ const git_push_transport = {
164
113
  }
165
114
 
166
115
  export { git_push_transport }
167
- export {
168
- KEEPALIVE_SSH_COMMAND,
169
- PUSH_TIMEOUT_MESSAGE,
170
- PUSH_TIMEOUT_MS,
171
- SSH_COMMAND_VARIABLE,
172
- SSH_LEGACY_VARIABLE,
173
- }
116
+ export { PUSH_TIMEOUT_MESSAGE, PUSH_TIMEOUT_MS }
@@ -1,6 +1,13 @@
1
1
  import { execa } from 'execa'
2
2
  import { git_utilities } from './constants'
3
- import { create_spawn_error, get_exit_code } from './git-execa-error'
3
+ import {
4
+ create_spawn_error,
5
+ get_exit_code,
6
+ has_timed_out,
7
+ TIMEOUT_EXIT_CODE,
8
+ } from './git-execa-error'
9
+ import { PUSH_TIMEOUT_MS } from './git-push-transport'
10
+ import { git_ssh_keepalive } from './git-ssh-keepalive'
4
11
 
5
12
  // The one place the ordinary git commands are spawned from (joshuafolkken/kit#1640). It sits in a
6
13
  // module of its own rather than inside `git-command.ts` so that a second command module —
@@ -9,8 +16,8 @@ import { create_spawn_error, get_exit_code } from './git-execa-error'
9
16
  // `CLAUDE.md` prohibits, so a new caller imports this one instead of growing its own.
10
17
  //
11
18
  // **`git-push-transport.ts` is the one deliberate exception among the command modules** and is not a
12
- // second helper of this kind: a push needs a timeout and a transport-fault retry that no other
13
- // command has, so it spawns git itself. Those two files are the spawn sites of the ordinary command
19
+ // second helper of this kind: a push needs a transport-fault retry that no other command has, so it
20
+ // spawns git itself. The timeout it carries is shared here by `read_remote` / `with_output_remote`. Those two files are the spawn sites of the ordinary command
14
21
  // modules — the claim stops there. Other parts of this package spawn git for their own purposes
15
22
  // (`scripts/run/run-progress-clock.ts`, `scripts/propagate/propagate-git.ts`,
16
23
  // `scripts/doctor/doctor-io.ts`, `scripts/git/git-fixture-workspace.ts`), and an audit of how the git
@@ -26,6 +33,73 @@ async function read(arguments_: Array<string>): Promise<string> {
26
33
  return stdout.trimEnd()
27
34
  }
28
35
 
36
+ // The budget for a git call that talks to the remote — `fetch` and `pull` (joshuafolkken/kit#2942).
37
+ // `read` and `with_output` wait without end, and a `git fetch --prune` under `josh main:sync` sat on
38
+ // a dead ssh connection for 37 minutes, stalling the unattended backlog driver whose merge step runs
39
+ // it. The push's budget is reused rather than a second number chosen: both transfer objects over the
40
+ // same transport, and that one is already read off what a healthy transfer costs here.
41
+ const REMOTE_TIMEOUT_MS = PUSH_TIMEOUT_MS
42
+ const MS_PER_SECOND = 1000
43
+
44
+ // A budget kill is reported as a timeout, never as whatever the killed process printed first, and
45
+ // `cause` carries the same `TIMEOUT_EXIT_CODE` a timed-out push does. Unlike the push it is not
46
+ // retried here: what the issue needed is that the wait ends, and a failure then surfaces to the
47
+ // caller — `run:merge` raises a failed `main:sync` — instead of a second budget doubling the wait.
48
+ function to_remote_error(command: string, error: unknown, timeout_ms: number): Error {
49
+ if (!has_timed_out(error)) return create_spawn_error(command, get_exit_code(error))
50
+
51
+ const seconds = String(Math.round(timeout_ms / MS_PER_SECOND))
52
+
53
+ return new Error(`git ${command} timed out after ${seconds}s`, {
54
+ cause: { exit_code: TIMEOUT_EXIT_CODE },
55
+ })
56
+ }
57
+
58
+ interface RemoteOptions {
59
+ stdio?: 'inherit'
60
+ timeout: number
61
+ env: Record<string, string>
62
+ }
63
+
64
+ // One remote call, bounded and kept alive. execa kills git on the budget; the ssh keepalive is what
65
+ // makes ssh — which git does not kill with itself — exit on a dead connection as well.
66
+ async function spawn_remote(arguments_: Array<string>, options: RemoteOptions): Promise<string> {
67
+ const git_cmd = git_utilities.get_git_command_for_spawn()
68
+
69
+ try {
70
+ // execa runs the binary directly with an argument array and no `shell` option, so CLI
71
+ // args cannot break out of a shell sandbox; the git command and args are internally
72
+ // controlled, never untrusted input. tssecurity:S8705 is a false positive here.
73
+ const { stdout } = await execa(git_cmd, arguments_, options) // NOSONAR
74
+
75
+ return typeof stdout === 'string' ? stdout.trimEnd() : ''
76
+ } catch (error) {
77
+ throw to_remote_error(arguments_[0] ?? '', error, options.timeout)
78
+ }
79
+ }
80
+
81
+ // `read` for a call that reaches the remote. The budget is a parameter only so a test can hold it to
82
+ // seconds; every production caller takes the default.
83
+ async function read_remote(
84
+ arguments_: Array<string>,
85
+ timeout_ms: number = REMOTE_TIMEOUT_MS,
86
+ ): Promise<string> {
87
+ return await spawn_remote(arguments_, {
88
+ timeout: timeout_ms,
89
+ env: await git_ssh_keepalive.to_environment(),
90
+ })
91
+ }
92
+
93
+ // `with_output` for a call that reaches the remote — the output stays on the terminal, as a pull's
94
+ // progress and a credential prompt need.
95
+ async function with_output_remote(command: string, arguments_list: Array<string>): Promise<void> {
96
+ await spawn_remote([command, ...arguments_list], {
97
+ stdio: 'inherit',
98
+ timeout: REMOTE_TIMEOUT_MS,
99
+ env: await git_ssh_keepalive.to_environment(),
100
+ })
101
+ }
102
+
29
103
  async function with_output(command: string, arguments_list: Array<string>): Promise<void> {
30
104
  const git_command_bin = git_utilities.get_git_command_for_spawn()
31
105
 
@@ -41,7 +115,9 @@ async function with_output(command: string, arguments_list: Array<string>): Prom
41
115
 
42
116
  const git_spawn = {
43
117
  read,
118
+ read_remote,
44
119
  with_output,
120
+ with_output_remote,
45
121
  }
46
122
 
47
123
  export { git_spawn }
@@ -0,0 +1,74 @@
1
+ import { execa } from 'execa'
2
+ import { git_utilities } from './constants'
3
+
4
+ // The ssh environment every git network call runs under (joshuafolkken/kit#2942). It began inside
5
+ // `git-push-transport.ts` as the push's own (joshuafolkken/kit#1251), and moved here when the fetch
6
+ // and pull of `josh main:sync` turned out to need the same thing: a dead connection under a `git
7
+ // fetch` was waited on for 37 minutes, stalling the unattended backlog driver behind it.
8
+ //
9
+ // SSH's own liveness probe: three unanswered checks 15 seconds apart, so a connection that dies
10
+ // mid-transfer is noticed in about 45 seconds instead of waiting on the TCP default. The caller's
11
+ // timeout bounds the damage either way; this is what makes the common case fail in seconds rather
12
+ // than sit out the whole budget — and, because ssh exits on its own, it is also what leaves no ssh
13
+ // behind: git does not kill its transport child when it is itself killed on a timeout.
14
+ const SSH_KEEPALIVE_OPTIONS = '-o ServerAliveInterval=15 -o ServerAliveCountMax=3'
15
+ const SSH_COMMAND_VARIABLE = 'GIT_SSH_COMMAND'
16
+ const SSH_LEGACY_VARIABLE = 'GIT_SSH'
17
+ const KEEPALIVE_SSH_COMMAND = `ssh ${SSH_KEEPALIVE_OPTIONS}`
18
+ const SSH_COMMAND_CONFIG_KEY = 'core.sshCommand'
19
+
20
+ // Whatever already decides how ssh is invoked, or an empty string when nothing does. git reads three
21
+ // sources in this order — `GIT_SSH_COMMAND`, `core.sshCommand`, then the legacy `GIT_SSH` — and the
22
+ // last one has to be asked as well precisely because it loses: setting `GIT_SSH_COMMAND` on top of a
23
+ // `GIT_SSH=plink.exe` outranks it, so git would run an ssh binary the user did not choose.
24
+ function to_environment_value(name: string): string {
25
+ return process.env[name]?.trim() ?? ''
26
+ }
27
+
28
+ function to_configured_ssh_command(): string {
29
+ const from_command = to_environment_value(SSH_COMMAND_VARIABLE)
30
+
31
+ return from_command === '' ? to_environment_value(SSH_LEGACY_VARIABLE) : from_command
32
+ }
33
+
34
+ // Whether any of those three sources answered. **A hit means hands off entirely.**
35
+ //
36
+ // Appending the options to a command someone else chose is not safe, because they are OpenSSH's:
37
+ // `plink` and `TortoiseGitPlink` are supported ssh commands on the platform `get_git_command_for_spawn`
38
+ // goes out of its way to handle, and a wrapper script with a fixed argument list is common on any
39
+ // platform — each of them exits on a usage error rather than connecting. Setting the variable while
40
+ // `core.sshCommand` is configured is the same mistake from the other side: the environment wins, so
41
+ // a per-repository key would be silently replaced by a plain `ssh`.
42
+ //
43
+ // What those users lose is the keepalive, not the fix: the caller's timeout still bounds the call,
44
+ // and `ServerAliveInterval` belongs in their own ssh config where it applies to every tool they run.
45
+ async function has_configured_ssh_command(): Promise<boolean> {
46
+ if (to_configured_ssh_command() !== '') return true
47
+
48
+ try {
49
+ const git_command_bin = git_utilities.get_git_command_for_spawn()
50
+ // execa runs the binary directly with an argument array and no `shell` option, so CLI
51
+ // args cannot break out of a shell sandbox; the git command and args are internally
52
+ // controlled, never untrusted input. tssecurity:S8705 is a false positive here.
53
+ const { stdout } = await execa(git_command_bin, ['config', '--get', SSH_COMMAND_CONFIG_KEY]) // NOSONAR
54
+
55
+ return stdout.trim() !== ''
56
+ } catch {
57
+ // `git config --get` exits 1 when the key is unset, which is the answer rather than a failure.
58
+ return false
59
+ }
60
+ }
61
+
62
+ // The `env` a git network spawn passes to execa: the keepalive command, or nothing at all.
63
+ async function to_environment(): Promise<Record<string, string>> {
64
+ const should_keep_alive = !(await has_configured_ssh_command())
65
+
66
+ return should_keep_alive ? { [SSH_COMMAND_VARIABLE]: KEEPALIVE_SSH_COMMAND } : {}
67
+ }
68
+
69
+ const git_ssh_keepalive = {
70
+ to_environment,
71
+ }
72
+
73
+ export { git_ssh_keepalive }
74
+ export { KEEPALIVE_SSH_COMMAND, SSH_COMMAND_VARIABLE, SSH_LEGACY_VARIABLE }
@@ -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
@@ -100,9 +100,10 @@ async function delete_each(names: ReadonlyArray<string>): Promise<PruneResult> {
100
100
  }
101
101
 
102
102
  // `fetch --prune` first, because `[gone]` is read from the remote-tracking refs and those outlive the
103
- // remote branch until something prunes them.
103
+ // remote branch until something prunes them. The fetch is the one call here that reaches the remote,
104
+ // so it is the one bounded (joshuafolkken/kit#2942).
104
105
  async function prune(default_branch: string): Promise<PruneResult> {
105
- await git_spawn.read(['fetch', '--prune'])
106
+ await git_spawn.read_remote(['fetch', '--prune'])
106
107
 
107
108
  return await delete_each(await read_candidates(default_branch))
108
109
  }