@joshuafolkken/kit 1.1005.0 → 1.1035.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.claude/settings.json +1 -5
  2. package/.claude/skills/dependency-update/SKILL.md +6 -3
  3. package/.claude/skills/epic-commands/SKILL.md +46 -17
  4. package/.claude/skills/workflow-commands/SKILL.md +8 -23
  5. package/.claude/skills/workflow-commands/backlogrun-child.md +4 -2
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -12
  7. package/.claude/skills/workflow-commands/fullrun-steps.md +4 -3
  8. package/.claude/skills/workflow-commands/fullrun.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  10. package/.claude/skills/workflow-commands/into-target.md +1 -1
  11. package/.claude/skills/workflow-commands/observation-filing.md +42 -43
  12. package/.claude/skills/workflow-commands/prerequisite.md +1 -1
  13. package/.codex/hooks.json +1 -5
  14. package/.github/workflows/sonar-qube.yml +1 -1
  15. package/AGENTS.md +4 -25
  16. package/CLAUDE.md +24 -38
  17. package/GEMINI.md +4 -24
  18. package/README.md +22 -58
  19. package/dist/CLAUDE.md +24 -38
  20. package/dist/hooks/batch-guard.js +4 -4
  21. package/dist/hooks/{chunk-WMIR5SNX.js → chunk-D67KVGGY.js} +3 -3
  22. package/dist/hooks/{chunk-EZLHXKJ6.js → chunk-EIFCAJRB.js} +70 -27
  23. package/dist/hooks/{chunk-KPZN42RT.js → chunk-GJT4BNVE.js} +3 -3
  24. package/dist/hooks/{chunk-PAXMLGSS.js → chunk-GLBDBJOK.js} +44 -24
  25. package/dist/hooks/{chunk-GBJUERYK.js → chunk-HOYJU5RM.js} +1 -1
  26. package/dist/hooks/{chunk-6PSTVOLH.js → chunk-HU66PWWL.js} +33 -57
  27. package/dist/hooks/{chunk-A2H5I2VD.js → chunk-KG6PZD72.js} +2 -2
  28. package/dist/hooks/{chunk-AWTFKKFW.js → chunk-XPLP27WA.js} +3 -8
  29. package/dist/hooks/{chunk-4GDR2YTV.js → chunk-Y27M6A7X.js} +4 -4
  30. package/dist/hooks/{chunk-56LGMJJV.js → chunk-ZXNE6NY3.js} +2 -2
  31. package/dist/hooks/codex-hook-adapter.js +10 -10
  32. package/dist/hooks/duplicate-read-guard.js +5 -5
  33. package/dist/hooks/format-edited.js +2 -2
  34. package/dist/hooks/investigation-guard.js +5 -5
  35. package/dist/hooks/pretool-guard.js +9 -9
  36. package/dist/hooks/stop-guard.js +11 -15
  37. package/dist/josh.js +38 -19
  38. package/dist/version/index.js +93 -41
  39. package/eslint/base.js +2 -11
  40. package/package.json +32 -6
  41. package/prompts/coding-standards.md +15 -290
  42. package/prompts/collaboration-workflow/cross-repo-epic.md +2 -51
  43. package/prompts/collaboration-workflow/epic-audit.md +2 -53
  44. package/prompts/collaboration-workflow/epic-bundle.md +2 -106
  45. package/prompts/collaboration-workflow/issue-template.md +4 -4
  46. package/prompts/collaboration-workflow/operating-rules.md +18 -75
  47. package/prompts/collaboration-workflow/principles.md +74 -0
  48. package/prompts/collaboration-workflow/residency.md +54 -14
  49. package/prompts/collaboration-workflow/rule-delivery.md +1 -11
  50. package/prompts/collaboration-workflow/upstream-interrupt.md +1 -1
  51. package/prompts/collaboration-workflow.md +22 -28
  52. package/scripts/adopt/adopt-logic.ts +2 -2
  53. package/scripts/adopt/adopt-toolkits.ts +1 -1
  54. package/scripts/adopt/adopt.ts +2 -2
  55. package/scripts/agent/agent-event.ts +2 -9
  56. package/scripts/agent/agent-exit-record.ts +2 -9
  57. package/scripts/backlog/backlog-budget-cli.ts +2 -6
  58. package/scripts/backlog/backlog-drive-cli.ts +20 -10
  59. package/scripts/backlog/backlog-drive.ts +50 -16
  60. package/scripts/backlog/backlog-offer-cli.ts +6 -5
  61. package/scripts/backlog/backlog-plan.ts +1 -1
  62. package/scripts/build/build-bin.ts +2 -1
  63. package/scripts/build/build-config-merge.ts +2 -1
  64. package/scripts/build/build-hooks.ts +24 -4
  65. package/scripts/build/build-managed-marker.ts +2 -1
  66. package/scripts/build/build-self-sync-guard.ts +2 -1
  67. package/scripts/build/build-version.ts +2 -1
  68. package/scripts/cases/cases-logic.ts +2 -6
  69. package/scripts/ci/pr-classification.ts +13 -11
  70. package/scripts/cost-runtime/cost-transcript.ts +4 -11
  71. package/scripts/doctor/doctor-consumer.ts +5 -12
  72. package/scripts/doctor/doctor-ports.ts +2 -2
  73. package/scripts/document/document-byte-budget.ts +8 -16
  74. package/scripts/document/document-section.ts +2 -6
  75. package/scripts/document/entry-read-budget.ts +1 -1
  76. package/scripts/{git/git-epic-add-body.ts → epic/epic-add-body.ts} +23 -23
  77. package/scripts/{git/git-epic-add-plan.ts → epic/epic-add-plan.ts} +23 -26
  78. package/scripts/{git/git-epic-add.ts → epic/epic-add.ts} +17 -17
  79. package/scripts/epic/epic-audit-cli.ts +4 -4
  80. package/scripts/epic/epic-audit-orphans.ts +2 -2
  81. package/scripts/epic/epic-audit-rationale.ts +1 -1
  82. package/scripts/epic/epic-audit.ts +1 -1
  83. package/scripts/{git/git-epic-body.ts → epic/epic-body.ts} +6 -6
  84. package/scripts/epic/epic-bundle-cli.ts +1 -1
  85. package/scripts/epic/epic-bundle.ts +2 -2
  86. package/scripts/epic/epic-candidate-confirm.ts +2 -1
  87. package/scripts/{git/git-epic-chains.ts → epic/epic-chains.ts} +4 -4
  88. package/{scripts-ai → scripts/epic}/epic-check.ts +5 -4
  89. package/{scripts-ai → scripts/epic}/epic-cli.ts +4 -4
  90. package/scripts/{git/git-epic-close-comment.ts → epic/epic-close-comment.ts} +9 -9
  91. package/scripts/{git/git-epic-close.ts → epic/epic-close.ts} +23 -19
  92. package/scripts/epic/epic-cross-repo.ts +21 -38
  93. package/scripts/{git/git-epic-decision.ts → epic/epic-decision.ts} +12 -14
  94. package/scripts/epic/epic-fetch.ts +3 -3
  95. package/scripts/epic/epic-graph.ts +3 -3
  96. package/scripts/epic/epic-index.ts +2 -2
  97. package/scripts/epic/epic-issue.ts +1 -1
  98. package/scripts/epic/epic-next.ts +3 -3
  99. package/scripts/{git/git-epic-parse.ts → epic/epic-parse.ts} +3 -3
  100. package/scripts/{git/git-epic-promote.ts → epic/epic-promote.ts} +10 -10
  101. package/scripts/{git/git-epic-read.ts → epic/epic-read.ts} +9 -9
  102. package/scripts/{git/git-epic-reconcile.ts → epic/epic-reconcile.ts} +18 -20
  103. package/scripts/{git/git-epic-reference.ts → epic/epic-reference.ts} +4 -23
  104. package/scripts/epic/epic-relation-recheck.ts +4 -3
  105. package/scripts/{git/git-epic-relations.ts → epic/epic-relations.ts} +5 -5
  106. package/scripts/{git/git-epic-remove-plan.ts → epic/epic-remove-plan.ts} +17 -19
  107. package/scripts/{git/git-epic-remove.ts → epic/epic-remove.ts} +13 -13
  108. package/scripts/{git/git-epic-run.ts → epic/epic-run.ts} +27 -27
  109. package/scripts/{git/git-epic-sections.ts → epic/epic-sections.ts} +6 -6
  110. package/scripts/{git/git-epic-shape.ts → epic/epic-shape.ts} +7 -7
  111. package/scripts/{git/git-epic-validate.ts → epic/epic-validate.ts} +13 -13
  112. package/{scripts-ai → scripts/epic}/epic.ts +17 -16
  113. package/scripts/gh/fix-gh-packages.ts +2 -1
  114. package/scripts/gh/gh-spawn.ts +12 -10
  115. package/scripts/git/constants.ts +1 -1
  116. package/scripts/git/git-branch.ts +10 -1
  117. package/scripts/git/git-command.ts +1 -1
  118. package/scripts/git/git-error.ts +2 -5
  119. package/{scripts-ai → scripts/git}/git-followup-finish.ts +7 -7
  120. package/scripts/git/git-followup-flush.ts +74 -48
  121. package/scripts/git/git-followup-issue-close.ts +2 -2
  122. package/scripts/git/git-followup-label.ts +1 -1
  123. package/scripts/git/git-followup-pending.ts +1 -1
  124. package/{scripts-ai → scripts/git}/git-followup-workflow.ts +23 -11
  125. package/scripts/git/git-gh-exec.ts +85 -26
  126. package/scripts/git/git-gh-issue-read.ts +2 -2
  127. package/scripts/git/git-gh-issue-rest.ts +2 -2
  128. package/scripts/git/git-gh-issue-write.ts +6 -6
  129. package/scripts/git/git-gh-pr-read.ts +0 -21
  130. package/scripts/git/git-gh-pr.ts +2 -23
  131. package/scripts/git/git-pr-followup-wrapup.ts +2 -6
  132. package/scripts/git/git-preflight.ts +3 -6
  133. package/scripts/git/git-staging.ts +21 -9
  134. package/{scripts-ai → scripts/git}/git-workflow.ts +11 -11
  135. package/scripts/git/github-issue-url.ts +1 -1
  136. package/scripts/git/issue-labels.ts +1 -1
  137. package/scripts/git/issue-reference.ts +19 -0
  138. package/scripts/git/main-merge.ts +2 -1
  139. package/scripts/git/main-sync.ts +3 -2
  140. package/scripts/git/schemas.ts +1 -1
  141. package/scripts/git/stash-pop-args.ts +7 -13
  142. package/scripts/git/stash-pop-cli.ts +2 -1
  143. package/{scripts-ai → scripts/git}/telegram-test-logic.ts +2 -2
  144. package/{scripts-ai → scripts/git}/telegram-test.ts +7 -7
  145. package/scripts/hooks/format-rewrite-notice.ts +2 -8
  146. package/scripts/init/init-ai-copy.ts +23 -14
  147. package/scripts/init/init.ts +8 -4
  148. package/scripts/init/start-steps.ts +2 -1
  149. package/scripts/init/start.ts +2 -1
  150. package/scripts/issue/issue-comment-cli.ts +2 -1
  151. package/scripts/issue/issue-file-cli.ts +2 -1
  152. package/scripts/issue/issue-fold-cli.ts +9 -16
  153. package/scripts/issue/issue-scout-cli.ts +8 -13
  154. package/scripts/issue/measure-rerun-cli.ts +7 -6
  155. package/scripts/josh/josh-commands-ai.ts +2 -2
  156. package/scripts/josh/josh-commands-backlog.ts +1 -1
  157. package/scripts/josh/josh-commands-development.ts +1 -1
  158. package/scripts/josh/josh-commands-workflow.ts +4 -4
  159. package/scripts/lane/lane-cli.ts +2 -1
  160. package/scripts/lane/lane-open.ts +3 -2
  161. package/scripts/lane/lane-registry.ts +3 -6
  162. package/scripts/lib/cli-flags.ts +20 -2
  163. package/scripts/lib/error-message.ts +9 -0
  164. package/scripts/lib/read-file.ts +26 -2
  165. package/scripts/observations/observation-ledger-home.ts +98 -26
  166. package/scripts/observations/observation-ledger-migrate.ts +26 -21
  167. package/scripts/observations/observation-ledger-prepare.ts +10 -20
  168. package/scripts/observations/observation-ledger.ts +58 -25
  169. package/scripts/observations/observations-flush-cli.ts +6 -23
  170. package/scripts/observations/observations-flush-landing.ts +2 -1
  171. package/scripts/observations/observations-flush.ts +29 -20
  172. package/scripts/package/package-scout-cli.ts +8 -16
  173. package/scripts/ports/port-command.ts +2 -1
  174. package/scripts/propagate/propagate-publish.ts +6 -7
  175. package/scripts/propagate/propagate-steps.ts +2 -1
  176. package/scripts/propagate/propagate.ts +3 -2
  177. package/scripts/refactor/import-graph.ts +2 -6
  178. package/scripts/registry-migration/migrate.ts +3 -2
  179. package/scripts/registry-migration/user-npmrc.ts +2 -2
  180. package/scripts/release/release-cli.ts +2 -1
  181. package/scripts/repo/repo-setting.ts +10 -8
  182. package/scripts/review/review-finding-ledger.ts +3 -3
  183. package/scripts/review/review-findings-cli.ts +4 -11
  184. package/scripts/review/review-record-cli.ts +14 -11
  185. package/scripts/review/review-record.ts +6 -16
  186. package/scripts/rules/decision-oracle.ts +2 -2
  187. package/scripts/rules/rule-body-guard.ts +2 -2
  188. package/scripts/rules/rule-value-cache.ts +99 -0
  189. package/scripts/rules/rule-value-cli.ts +10 -9
  190. package/scripts/rules/rule-value.ts +1 -1
  191. package/scripts/run/run-carry-args.ts +2 -6
  192. package/scripts/run/run-carry-cli.ts +5 -7
  193. package/scripts/run/run-cut-args.ts +7 -13
  194. package/scripts/run/run-ending-cli.ts +4 -7
  195. package/scripts/run/run-event-stream.ts +2 -5
  196. package/scripts/run/run-liveness-cli.ts +4 -7
  197. package/scripts/run/run-progress-args.ts +2 -6
  198. package/scripts/run/run-progress-cli.ts +2 -1
  199. package/scripts/run/run-tail-cli.ts +3 -3
  200. package/scripts/run/run-tidy-stashes.ts +7 -14
  201. package/scripts/run/run-wake-cli.ts +2 -6
  202. package/scripts/run/run-wake-driver.ts +15 -2
  203. package/scripts/run/run-wake-session.ts +14 -4
  204. package/scripts/safe-chain/project-config.ts +3 -6
  205. package/scripts/security/security-audit-provision.ts +6 -7
  206. package/scripts/sonar/sonar-hotspots-cli.ts +2 -1
  207. package/scripts/sync/directory-copy-guard.ts +2 -1
  208. package/scripts/sync/sync-ai-files.ts +3 -3
  209. package/scripts/sync/template-source-logic.ts +3 -4
  210. package/scripts/test/e2e-retry-check.ts +3 -6
  211. package/scripts/ui/ui-routes-cli.ts +3 -6
  212. package/scripts/version/bump-version.ts +2 -1
  213. package/scripts/version/version-remote.ts +18 -27
  214. package/scripts/version/version-targets.ts +3 -3
  215. package/.claude/skills/workflow-commands/rule-residency.md +0 -267
  216. package/evals/scenarios/consult-not-execute.json +0 -20
  217. package/evals/scenarios/no-implicit-workflow.json +0 -26
  218. package/evals/scenarios/overrides-both-locations.json +0 -29
  219. package/evals/scenarios/upstream-interrupt.json +0 -23
  220. package/evals/scenarios/workflow-skill-loaded.json +0 -16
  221. package/prompts/agent-lint-protocol.json +0 -79
  222. package/prompts/agent-rules.md +0 -63
  223. package/prompts/collaboration-workflow/consultation-vs-execution.md +0 -8
  224. package/prompts/collaboration-workflow/distributed-docs.md +0 -7
  225. package/prompts/collaboration-workflow/durable-rules.md +0 -24
  226. package/prompts/collaboration-workflow/latest-first.md +0 -14
  227. package/prompts/collaboration-workflow/no-clones.md +0 -11
  228. package/prompts/collaboration-workflow/simplicity-first.md +0 -9
  229. package/prompts/collaboration-workflow/single-source-rules.md +0 -13
  230. package/scripts/build/shim-shell.ts +0 -25
  231. package/scripts/claude/skill-meta.ts +0 -37
  232. package/scripts/document/document-scan.ts +0 -108
  233. package/scripts/document/generate-catalog.ts +0 -69
  234. package/scripts/document/retired-phrases.ts +0 -143
  235. package/scripts/document/review-agent-type.ts +0 -46
  236. package/scripts/document/single-source.ts +0 -73
  237. package/scripts/git/git-fixture-workspace.ts +0 -88
  238. package/scripts/hooks/format-edited-file-fixtures.ts +0 -109
  239. package/scripts/hooks/prompt-hooks.ts +0 -38
  240. package/scripts/init/hook-command-bootstrap.ts +0 -83
  241. package/scripts/issue/issue-logic.ts +0 -50
  242. package/scripts/josh/__snapshots__/josh-command-map.test.ts.snap +0 -1227
  243. package/scripts/josh/josh-script-reader.ts +0 -11
  244. package/scripts/package/production-dependency-graph.ts +0 -68
  245. package/scripts/retrospective/retrospective-cli.ts +0 -97
  246. package/scripts/retrospective/retrospective.ts +0 -127
  247. package/scripts/rules/delivered-rules-harness.ts +0 -72
  248. package/scripts/rules/top-level-scripts-guard.ts +0 -47
  249. package/scripts/run/run-carry-flush.ts +0 -58
  250. package/scripts/run/run-transcript.golden.txt +0 -57
  251. package/scripts/run/run-wake-work.ts +0 -108
  252. package/scripts/sync/dependabot-pin-logic.ts +0 -135
  253. package/scripts/test/classify-isolation.ts +0 -149
  254. package/scripts/test/http-fault-injection.ts +0 -79
  255. package/scripts/test/josh-harness-environment.ts +0 -260
  256. package/scripts/test/josh-harness.ts +0 -96
  257. package/scripts/test/pilot-files.ts +0 -452
  258. package/scripts/test/pilot-measure.ts +0 -78
  259. package/scripts/test/test-network-guard.ts +0 -453
  260. package/scripts/test/test-repository-guard.ts +0 -317
  261. package/scripts/test/test-state-guard.ts +0 -85
  262. package/scripts/test/test-stdout-guard.ts +0 -54
  263. package/scripts/test/test-telegram-guard.ts +0 -90
  264. package/scripts/test/unit-guard-environment.ts +0 -12
  265. package/scripts/test/unit-projects.ts +0 -139
  266. package/scripts/test/vitest-include-globs.ts +0 -12
  267. package/scripts/time-runtime/time-batch-guard-fixtures.ts +0 -66
  268. package/scripts-ai/completion-gate.ts +0 -13
  269. package/scripts-ai/environment-loader.ts +0 -9
  270. /package/{scripts-ai → scripts/epic}/epic-cli-argv.ts +0 -0
  271. /package/{scripts-ai → scripts/epic}/epic-cli-remove.ts +0 -0
  272. /package/{scripts-ai → scripts/git}/followup-issue-number.ts +0 -0
@@ -1,19 +1,19 @@
1
- import { git_epic_parse, type DeclarationState } from './git-epic-parse'
2
- import { EPIC_LABEL, has_label_name } from './issue-labels'
3
- import { parse_json_object_safe } from './parse-json-array'
4
- import { epic_subject_schema } from './schemas'
1
+ import { EPIC_LABEL, has_label_name } from '#scripts/git/issue-labels'
2
+ import { parse_json_object_safe } from '#scripts/git/parse-json-array'
3
+ import { epic_subject_schema } from '#scripts/git/schemas'
4
+ import { epic_parse, type DeclarationState } from './epic-parse'
5
5
 
6
6
  // The four requirements an epic has to satisfy are otherwise enforced only by an agent reading the
7
7
  // procedure carefully, and three of the four fail silently: a missing label or a bare `#N` child
8
- // list makes `git-epic-close.ts` skip the epic forever, and prose dependencies make the batch read
8
+ // list makes `epic-close.ts` skip the epic forever, and prose dependencies make the batch read
9
9
  // as unordered so a skipped `--add-blocked-by` is never reported. Checking them here reuses
10
- // `git-epic-parse.ts` rather than restating the patterns, so "what the auto-close can read" and
10
+ // `epic-parse.ts` rather than restating the patterns, so "what the auto-close can read" and
11
11
  // "what this check accepts" are one definition instead of two that can drift.
12
12
 
13
13
  interface EpicSubject {
14
14
  number: number
15
15
  // The epic's own state, as GitHub returned it (`OPEN` / `CLOSED`, casing unnormalized). Only
16
- // `git-epic-add.ts` reads it — to refuse adding a child to a closed epic — so it is optional: a read
16
+ // `epic-add.ts` reads it — to refuse adding a child to a closed epic — so it is optional: a read
17
17
  // that came back without the field leaves it undefined, which the refusal treats as "not confirmed
18
18
  // closed" rather than blocking (joshuafolkken/kit#2337).
19
19
  state?: string | undefined
@@ -65,7 +65,7 @@ function format_children(children: ReadonlyArray<number>): string {
65
65
  }
66
66
 
67
67
  function check_task_list(subject: EpicSubject): CheckResult {
68
- const children = git_epic_parse.parse_task_list_issue_numbers(subject.body)
68
+ const children = epic_parse.parse_task_list_issue_numbers(subject.body)
69
69
  const is_passing = children.length > 0
70
70
 
71
71
  return {
@@ -94,11 +94,11 @@ function describe_dependencies(state: DeclarationState): string {
94
94
  // each of them. The literal is compared against the constant the generator emits, and the chain
95
95
  // against the one the link reader parses, so no two of them can drift into disagreement.
96
96
  function check_dependencies(subject: EpicSubject): CheckResult {
97
- const state = git_epic_parse.read_declaration(subject.body)
97
+ const state = epic_parse.read_declaration(subject.body)
98
98
 
99
99
  return {
100
100
  name: 'dependencies section',
101
- is_passing: git_epic_parse.is_declaration_readable(state),
101
+ is_passing: epic_parse.is_declaration_readable(state),
102
102
  detail: describe_dependencies(state),
103
103
  }
104
104
  }
@@ -107,7 +107,7 @@ function check_dependencies(subject: EpicSubject): CheckResult {
107
107
  // deliberately leaves such an epic open. Reporting it here is what turns "stayed open silently"
108
108
  // into "was told to close it manually".
109
109
  function check_external_children(subject: EpicSubject): CheckResult {
110
- const has_external = git_epic_parse.has_external_task_list_entry(subject.body)
110
+ const has_external = epic_parse.has_external_task_list_entry(subject.body)
111
111
 
112
112
  return {
113
113
  name: 'auto-close eligibility',
@@ -144,12 +144,12 @@ function format_check_report(epic_number: number, results: ReadonlyArray<CheckRe
144
144
  return [...lines, '', summary].join('\n')
145
145
  }
146
146
 
147
- const git_epic_validate = {
147
+ const epic_validate = {
148
148
  parse_epic_subject,
149
149
  validate_epic,
150
150
  is_epic_valid,
151
151
  format_check_report,
152
152
  }
153
153
 
154
- export { git_epic_validate }
154
+ export { epic_validate }
155
155
  export type { CheckResult, EpicSubject }
@@ -2,19 +2,20 @@
2
2
  /**
3
3
  * Create an epic Issue that satisfies every requirement the auto-close and the order warning read.
4
4
  *
5
- * Usage: tsx scripts-ai/epic.ts "<title>" <N1> <N2> ... [--ordered] [--rationale-file <path|->]
5
+ * Usage: tsx scripts/epic/epic.ts "<title>" <N1> <N2> ... [--ordered] [--rationale-file <path|->]
6
6
  * [--origin <owner/repo#N>]
7
- * tsx scripts-ai/epic.ts --promote <N> <N1> <N2> ... [same flags]
8
- * tsx scripts-ai/epic.ts --add <E> <N1> <N2> ... [--before <M> | --after <M>]
7
+ * tsx scripts/epic/epic.ts --promote <N> <N1> <N2> ... [same flags]
8
+ * tsx scripts/epic/epic.ts --add <E> <N1> <N2> ... [--before <M> | --after <M>]
9
9
  * [--decision-file <path|->]
10
- * tsx scripts-ai/epic.ts --remove <E> <M> <N> ... [--decision-file <path|->]
10
+ * tsx scripts/epic/epic.ts --remove <E> <M> <N> ... [--decision-file <path|->]
11
11
  */
12
- import { git_epic_add, type AddChildrenInput } from '../scripts/git/git-epic-add'
13
- import { git_epic_reconcile } from '../scripts/git/git-epic-reconcile'
14
- import { git_epic_remove } from '../scripts/git/git-epic-remove'
15
- import { git_epic_run } from '../scripts/git/git-epic-run'
16
- import { git_gh_command } from '../scripts/git/git-gh-command'
12
+ import { git_gh_command } from '#scripts/git/git-gh-command'
13
+ import { error_text } from '#scripts/lib/error-message'
14
+ import { epic_add, type AddChildrenInput } from './epic-add'
17
15
  import { epic_cli, type AddArguments, type CrossRepoAddTarget } from './epic-cli'
16
+ import { epic_reconcile } from './epic-reconcile'
17
+ import { epic_remove } from './epic-remove'
18
+ import { epic_run } from './epic-run'
18
19
 
19
20
  const ARGV_OFFSET = 2
20
21
  const FLAGS = '[--ordered] [--rationale-file <path|->] [--origin <owner/repo#N>]'
@@ -37,7 +38,7 @@ async function run_promotion(argv: ReadonlyArray<string>): Promise<number> {
37
38
  return FAILURE_EXIT_CODE
38
39
  }
39
40
 
40
- return await git_epic_run.promote_epic({
41
+ return await epic_run.promote_epic({
41
42
  epic_number: parsed.epic_number,
42
43
  children: parsed.children,
43
44
  rationale: epic_cli.read_rationale(parsed.rationale_path),
@@ -67,7 +68,7 @@ function to_add_input(parsed: AddArguments): AddChildrenInput {
67
68
  async function run_qualified_addition(found: CrossRepoAddTarget): Promise<number> {
68
69
  const local = epic_cli.resolve_local_add(found, await git_gh_command.repo_get_name_with_owner())
69
70
 
70
- if (local !== undefined) return await git_epic_add.add_children(to_add_input(local))
71
+ if (local !== undefined) return await epic_add.add_children(to_add_input(local))
71
72
 
72
73
  console.error(epic_cli.format_cross_repo_refusal(found))
73
74
 
@@ -109,7 +110,7 @@ async function run_addition(argv: ReadonlyArray<string>): Promise<number> {
109
110
 
110
111
  if (parsed === undefined) return await refuse_addition(argv)
111
112
 
112
- return await git_epic_add.add_children(to_add_input(parsed))
113
+ return await epic_add.add_children(to_add_input(parsed))
113
114
  }
114
115
 
115
116
  // Deleting a declared order. No position and no `--ordered`: the path itself says which orders go,
@@ -126,7 +127,7 @@ async function run_removal(argv: ReadonlyArray<string>): Promise<number> {
126
127
  return FAILURE_EXIT_CODE
127
128
  }
128
129
 
129
- return await git_epic_remove.remove_order({
130
+ return await epic_remove.remove_order({
130
131
  epic_number: parsed.epic_number,
131
132
  path: parsed.path,
132
133
  decision: epic_cli.read_decision(parsed.decision_path),
@@ -144,7 +145,7 @@ async function run_reconciliation(argv: ReadonlyArray<string>): Promise<number>
144
145
  return FAILURE_EXIT_CODE
145
146
  }
146
147
 
147
- return await git_epic_reconcile.reconcile_epic(parsed.epic_number)
148
+ return await epic_reconcile.reconcile_epic(parsed.epic_number)
148
149
  }
149
150
 
150
151
  async function run_creation(argv: ReadonlyArray<string>): Promise<number> {
@@ -156,7 +157,7 @@ async function run_creation(argv: ReadonlyArray<string>): Promise<number> {
156
157
  return FAILURE_EXIT_CODE
157
158
  }
158
159
 
159
- return await git_epic_run.create_epic({
160
+ return await epic_run.create_epic({
160
161
  title: parsed.title,
161
162
  children: parsed.children,
162
163
  rationale: epic_cli.read_rationale(parsed.rationale_path),
@@ -187,6 +188,6 @@ async function main(): Promise<void> {
187
188
  try {
188
189
  await main()
189
190
  } catch (error) {
190
- console.error(`✖ ${error instanceof Error ? error.message : String(error)}`)
191
+ console.error(`✖ ${error_text.message_of(error)}`)
191
192
  process.exit(FAILURE_EXIT_CODE)
192
193
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
+ import { file_reader } from '#scripts/lib/read-file'
4
5
  import { z } from 'zod'
5
6
  import { fix_gh_packages_logic, type LockfilePackage } from './fix-gh-packages-logic'
6
7
  import { gh_cli_token } from './gh-cli-token'
@@ -19,7 +20,7 @@ const npm_packument_schema = z.looseObject({
19
20
  type ParsedPackages = Record<string, LockfilePackage>
20
21
 
21
22
  function read_file(file_path: string): string {
22
- return existsSync(file_path) ? readFileSync(file_path, 'utf8') : ''
23
+ return file_reader.read_file_or_empty(file_path)
23
24
  }
24
25
 
25
26
  // Resolution order: NODE_AUTH_TOKEN → a literal token in the project .npmrc → `gh auth token`.
@@ -1,27 +1,29 @@
1
1
  import { git_gh_api_path } from '#scripts/git/git-gh-api-path'
2
2
  import { git_gh_exec } from '#scripts/git/git-gh-exec'
3
3
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
4
- import { execaSync } from 'execa'
5
4
 
6
5
  // The synchronous twin of `git_gh_repo.repo_get_name_with_owner`: same fact, read the same way, but
7
- // through `execaSync` because `josh init` / `josh sync` / `josh doctor` decide before they can await.
6
+ // synchronously because `josh init` / `josh sync` / `josh doctor` decide before they can await.
8
7
  // It asks REST for the same reason — `gh repo view` goes through GraphQL, which a cloud session is
9
8
  // refused (403), and a repository that reads as unresolved makes `init` / `sync` skip
10
9
  // `sonar-project.properties` outright (joshuafolkken/kit#1023). The path comes from the shared
11
10
  // builder rather than being spelled out again here.
12
11
  //
13
- // `timeout` is spread in rather than passed as `undefined`: execa's options type does not admit an
14
- // undefined value for it, and an explicit `undefined` would not mean "no timeout" anyway.
12
+ // An unbounded lookup asks for `NO_BUDGET`, execa's own "no timeout": the shared layer otherwise
13
+ // applies its default budget, which would change what `init` / `sync` wait for.
14
+ const NO_BUDGET = 0
15
+
15
16
  function fetch_repo_name(timeout_ms: number | undefined): string | undefined {
16
- const result = execaSync('gh', ['api', git_gh_api_path.repo_api_path(), '--jq', '.full_name'], {
17
- ...git_gh_exec.direct_environment(),
17
+ const stdout = git_gh_exec.read_gh_api_sync({
18
+ path: git_gh_api_path.repo_api_path(),
19
+ jq_filter: '.full_name',
18
20
  cwd: PROJECT_ROOT,
19
- reject: false,
20
- ...(timeout_ms !== undefined && { timeout: timeout_ms }),
21
+ timeout_ms: timeout_ms ?? NO_BUDGET,
21
22
  })
22
- if (result.exitCode !== 0 || !result.stdout) return undefined
23
23
 
24
- return result.stdout.trim() || undefined
24
+ const name = stdout?.trim()
25
+
26
+ return name === '' ? undefined : name
25
27
  }
26
28
 
27
29
  // The unbounded lookup, for callers whose *writes* depend on the answer. A timeout would surface as
@@ -10,7 +10,7 @@ const GIT_COMMAND_UNIX = '/usr/bin/git'
10
10
  // modules (joshuafolkken/kit#1640) and a copy in each is a second answer to one question.
11
11
  const PORCELAIN_FLAG = '--porcelain'
12
12
  // The untracked-files mode every porcelain status reading passes (`git-command.ts` carries why it is
13
- // passed and why it is `all`), shared so `run-carry-flush.ts`'s own reading cannot drift from it.
13
+ // passed and why it is `all`), shared so another module's own reading cannot drift from it.
14
14
  const UNTRACKED_FILES_FLAG = '--untracked-files=all'
15
15
 
16
16
  function get_git_command(): string {
@@ -75,7 +75,7 @@ async function pull_latest(): Promise<void> {
75
75
  )
76
76
  }
77
77
 
78
- const ISSUE_PREFIX_PATTERN = /^\d+-/u
78
+ const ISSUE_PREFIX_PATTERN = /^(\d+)-/u
79
79
 
80
80
  function has_same_issue_prefix(branch_a: string, branch_b: string): boolean {
81
81
  const prefix_a = ISSUE_PREFIX_PATTERN.exec(branch_a)?.[0]
@@ -84,6 +84,14 @@ function has_same_issue_prefix(branch_a: string, branch_b: string): boolean {
84
84
  return prefix_a !== undefined && prefix_a === prefix_b
85
85
  }
86
86
 
87
+ // The issue a branch names, or `undefined` when it does not lead with one — `1937-lane` and
88
+ // `1937-add-a-scope` both read 1937, while `main` reads nothing.
89
+ function issue_from_branch(branch: string): number | undefined {
90
+ const digits = ISSUE_PREFIX_PATTERN.exec(branch)?.[1]
91
+
92
+ return digits === undefined ? undefined : Number(digits)
93
+ }
94
+
87
95
  // The one rule `check_and_create_branch` refuses on, kept pure so `git-preflight.ts` asks it before
88
96
  // anything is pulled, switched or staged.
89
97
  function is_mismatch(
@@ -133,6 +141,7 @@ const git_branch = {
133
141
  // second copy of the pattern (joshuafolkken/kit#1497). A copy is what let `lane/<N>` ship: nothing
134
142
  // asserted that the name `lane:open` writes is one this function accepts.
135
143
  has_same_issue_prefix,
144
+ issue_from_branch,
136
145
  }
137
146
 
138
147
  export { git_branch }
@@ -411,7 +411,7 @@ async function push(): Promise<void> {
411
411
  // the same thing: it is on the default branch, bringing it up to date before doing something else —
412
412
  // `main-sync.ts` for `josh ms`, `release-cli.ts` / `release-publish.ts` around the release pull
413
413
  // request, `scripts-ai/prep.ts` before it snapshots the overrides, and `git-branch.ts` →
414
- // `pull_latest` — reached from `scripts-ai/git-workflow.ts`, so it runs on every `josh git` /
414
+ // `pull_latest` — reached from `git-workflow.ts`, so it runs on every `josh git` /
415
415
  // `josh pr` started from the default branch, which makes it the hottest of the five rather than a
416
416
  // dormant one. **None of them is
417
417
  // asking to absorb divergence**, so `merge_branch`'s reasoning inverts here: a default branch that
@@ -1,9 +1,6 @@
1
+ import { error_text } from '#scripts/lib/error-message'
1
2
  import { has_stderr_field } from './git-gh-exec'
2
3
 
3
- function get_error_message(error: unknown): string {
4
- return error instanceof Error ? error.message : String(error)
5
- }
6
-
7
4
  function get_stderr_from_error(cause: Error): string | undefined {
8
5
  if (!has_stderr_field(cause)) return undefined
9
6
 
@@ -46,7 +43,7 @@ function display_error_details(cause: unknown): void {
46
43
  }
47
44
 
48
45
  function handle(error: unknown): void {
49
- const error_message = get_error_message(error)
46
+ const error_message = error_text.message_of(error)
50
47
 
51
48
  console.error('')
52
49
  console.error('❌ Error:', error_message)
@@ -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.