@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
@@ -65,13 +65,17 @@ const SKIP_REASONS: Readonly<Record<string, string>> = {
65
65
  missing_checkout: 'no local checkout at the mapped path — reported, not cloned',
66
66
  unreadable: 'package.json could not be read',
67
67
  not_selected: 'not the repository --target named',
68
+ carried_above: 'delivered by the propagation of a toolkit above this one',
68
69
  }
69
70
 
70
71
  // The reason text for a candidate that is not going to be processed. A missing local checkout is
71
72
  // reported rather than cloned: propagation writes to a working tree, and creating one nobody asked
72
73
  // for is not a step this command may take on its own.
73
74
  function skip_reason(target: PropagateTarget): string {
74
- return SKIP_REASONS[target.state] ?? 'not eligible'
75
+ const reason = SKIP_REASONS[target.state] ?? 'not eligible'
76
+ if (target.carriers === undefined) return reason
77
+
78
+ return `${reason}: ${target.carriers.join(', ')}`
75
79
  }
76
80
 
77
81
  const LEFTOVER_NOTE = 'upgrade/sync changes left uncommitted'
@@ -3,6 +3,7 @@ import { git_gh_exec } from '#scripts/git/git-gh-exec'
3
3
  import { git_gh_issue_write } from '#scripts/git/git-gh-issue-write'
4
4
  import { OTHER_CHANGE_LABEL } from '#scripts/git/issue-labels'
5
5
  import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
6
+ import { error_text } from '#scripts/lib/error-message'
6
7
  import { build_upgrade_shell_command } from '#scripts/version/upgrade-shell-command'
7
8
  import { create_version_command_config } from '#scripts/version/version-command-config'
8
9
  import { execaSync } from 'execa'
@@ -87,13 +88,28 @@ function release_names(releases: ReadonlyArray<Release>): string {
87
88
  return `${names.slice(0, -1).join(NAME_SEPARATOR)}${LAST_SEPARATOR}${last}`
88
89
  }
89
90
 
91
+ // Each release with its own version, for a plan whose releases disagree on one — a toolkit's
92
+ // propagation carries the base packages at the versions that toolkit was verified with
93
+ // (joshuafolkken/kit#2879), so one shared "to <version>" would name a version most of them are not.
94
+ function versioned_names(releases: ReadonlyArray<Release>): string {
95
+ const named = releases.map((release) => ({
96
+ ...release,
97
+ package_name: `${release.package_name} to ${release.version}`,
98
+ }))
99
+
100
+ return `Upgrade ${release_names(named)}`
101
+ }
102
+
90
103
  // One release reproduces `issue_title` exactly, which is what keeps a `propagate` run's issue title
91
104
  // unchanged by the generalization.
92
105
  function plan_title(releases: ReadonlyArray<Release>): string {
93
106
  const [first] = releases
94
107
  if (first === undefined) return EMPTY_PLAN_TITLE
108
+ const is_one_version = releases.every((release) => release.version === first.version)
95
109
 
96
- return issue_title(release_names(releases), first.version)
110
+ return is_one_version
111
+ ? issue_title(release_names(releases), first.version)
112
+ : versioned_names(releases)
97
113
  }
98
114
 
99
115
  function plan_body(plan: ReleasePlan): string {
@@ -235,7 +251,7 @@ function precheck_step(target: PropagateTarget, step: string): StepResult {
235
251
  // multi-line detail would print raw JSON as an unindented second line and glue that warning to the
236
252
  // end of it, where nobody reading the report would see it.
237
253
  function to_issue_detail(error: unknown): string {
238
- const message = error instanceof Error ? error.message : String(error)
254
+ const message = error_text.message_of(error)
239
255
 
240
256
  return message.replaceAll('\n', ' ').trim()
241
257
  }
@@ -20,9 +20,17 @@ const MANIFEST_NAME = 'package.json'
20
20
  const NODE_MODULES = 'node_modules'
21
21
 
22
22
  // Why a candidate is not going to be processed, or that it is. `not_selected` is the one state set
23
- // after classification: a candidate `--target` left out (joshuafolkken/kit#2755).
23
+ // after classification: a candidate `--target` left out (joshuafolkken/kit#2755). `carried_above` is
24
+ // the other: a consumer that installs a toolkit above the supplier, which that toolkit's own
25
+ // propagation delivers instead (joshuafolkken/kit#2879).
24
26
  type TargetState =
25
- 'ready' | 'up_to_date' | 'not_downstream' | 'missing_checkout' | 'unreadable' | 'not_selected'
27
+ | 'ready'
28
+ | 'up_to_date'
29
+ | 'not_downstream'
30
+ | 'missing_checkout'
31
+ | 'unreadable'
32
+ | 'not_selected'
33
+ | 'carried_above'
26
34
 
27
35
  interface PropagateTarget {
28
36
  repo: string
@@ -31,6 +39,9 @@ interface PropagateTarget {
31
39
  // The range the consumer currently declares, when it declares one at all. Reported so a skip can
32
40
  // say what it skipped rather than only that it did.
33
41
  declared_range?: string
42
+ // The toolkits above the supplier this consumer installs, when it is `carried_above` — named in
43
+ // the skip so the report says which propagation will deliver it.
44
+ carriers?: ReadonlyArray<string>
34
45
  }
35
46
 
36
47
  // Only the fields propagation reads. `passthrough` is deliberate — a consumer manifest carries far
@@ -2,17 +2,16 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { repo_discovery } from '#scripts/discovery/repo-discovery'
4
4
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
5
- import { refuse_unknown_flags } from '#scripts/lib/cli-flags'
5
+ import { cli_flags } from '#scripts/lib/cli-flags'
6
6
  import { status_icons } from '#scripts/lib/status-icons'
7
- import { self_sync_guard } from '#scripts/self-sync-guard/self-sync-guard-logic'
8
- import { KIT_PACKAGE_NAME } from '#scripts/version/kit-descriptor'
9
7
  import { derive_versions_endpoint } from '#scripts/version/version-command-config'
10
8
  import { version_targets } from '#scripts/version/version-targets'
11
9
  import { propagate_git } from './propagate-git'
10
+ import { propagate_plan } from './propagate-plan'
12
11
  import { propagate_publish } from './propagate-publish'
13
12
  import { propagate_run } from './propagate-run'
14
13
  import { propagate_select, type TargetSelection } from './propagate-select'
15
- import { propagate_steps } from './propagate-steps'
14
+ import { propagate_steps, type ReleasePlan } from './propagate-steps'
16
15
  import { propagate_targets, type PropagateTarget } from './propagate-targets'
17
16
 
18
17
  // `josh propagate` — carry the release this repository just published into every consumer checked
@@ -72,12 +71,14 @@ function parse_options(argv: ReadonlyArray<string>): RunOptions {
72
71
  ...(target !== undefined && { target }),
73
72
  }
74
73
  const usage =
75
- target_usage ?? refuse_unknown_flags(rest, [...KNOWN_FLAGS, TARGET_USAGE], 'propagate')
74
+ target_usage ??
75
+ cli_flags.refuse_unknown_flags(rest, [...KNOWN_FLAGS, TARGET_USAGE], 'propagate')
76
76
 
77
77
  return usage === undefined ? options : { ...options, usage }
78
78
  }
79
79
 
80
- // Propagation runs from the supplier's own repository, and only there.
80
+ // Propagation runs from the supplier's own repository, and only there — kit's, or a toolkit's built
81
+ // on it (joshuafolkken/kit#2879).
81
82
  //
82
83
  // This is also what decides who propagates when several sessions are running: in the
83
84
  // per-repository concurrency model there is one session per checkout, so the session standing in
@@ -85,13 +86,7 @@ function parse_options(argv: ReadonlyArray<string>): RunOptions {
85
86
  // convention enforced at the boundary, not a lock — two checkouts of the supplier would both pass,
86
87
  // which is why each consumer is additionally refused unless its working tree is clean.
87
88
  function refuse_outside_source_repository(project_root: string): string | undefined {
88
- if (self_sync_guard.read_package_name(project_root) === KIT_PACKAGE_NAME) return undefined
89
-
90
- return [
91
- `Refusing to propagate: this is not ${KIT_PACKAGE_NAME}'s own repository.`,
92
- 'Propagation carries a published release outward, so it runs from the package that published it.',
93
- `Run it from the ${KIT_PACKAGE_NAME} checkout instead.`,
94
- ].join('\n')
89
+ return propagate_plan.resolve_supplier(project_root).refusal
95
90
  }
96
91
 
97
92
  // The version to carry: this repository's own declared version, which is what the merge published.
@@ -143,10 +138,14 @@ function resolve_run_version(project_root: string, is_dry_run = false): RunVersi
143
138
  return resolve_supplier_version(project_root, is_dry_run)
144
139
  }
145
140
 
146
- async function await_publish(target_version: string, is_skipped: boolean): Promise<boolean> {
141
+ async function await_publish(
142
+ package_name: string,
143
+ target_version: string,
144
+ is_skipped: boolean,
145
+ ): Promise<boolean> {
147
146
  if (is_skipped) return true
148
- console.info(`Waiting for ${KIT_PACKAGE_NAME}@${target_version} to appear in the registry…`)
149
- const endpoint = derive_versions_endpoint(KIT_PACKAGE_NAME)
147
+ console.info(`Waiting for ${package_name}@${target_version} to appear in the registry…`)
148
+ const endpoint = derive_versions_endpoint(package_name)
150
149
  const result = await propagate_publish.wait_for_publish(endpoint, target_version)
151
150
 
152
151
  if (result.state === 'published') {
@@ -178,49 +177,65 @@ function announce_run_version(resolved: RunVersion): string | undefined {
178
177
 
179
178
  interface RunPlan {
180
179
  version: string
180
+ releases: ReleasePlan
181
181
  targets: ReadonlyArray<PropagateTarget>
182
182
  }
183
183
 
184
184
  // The consumers this run will consider, narrowed by `--target`. Resolved before the publish wait so
185
- // a mistyped name fails at once instead of after minutes of polling the registry.
186
- function resolve_run_targets(version: string, target: string | undefined): TargetSelection {
185
+ // a mistyped name fails at once instead of after minutes of polling the registry. A consumer that
186
+ // installs a toolkit above the supplier is left to that toolkit's propagation (joshuafolkken/kit#2879).
187
+ function resolve_run_targets(
188
+ releases: ReleasePlan,
189
+ version: string,
190
+ target: string | undefined,
191
+ ): TargetSelection {
187
192
  const map = repo_discovery.discover_repositories(PROJECT_ROOT)
188
- const targets = propagate_targets.resolve_targets(map, KIT_PACKAGE_NAME, version)
193
+ const supplier = propagate_plan.supplier_name(releases)
194
+ const targets = propagate_targets
195
+ .resolve_targets(map, supplier, version)
196
+ .map((candidate) => propagate_plan.mark_carried(candidate, releases))
189
197
 
190
198
  return propagate_select.select_target(targets, target)
191
199
  }
192
200
 
193
- // The version and the consumers, or nothing once whatever stopped the run has been printed.
201
+ // Print why the run stopped before any consumer was touched.
202
+ function refuse_run(refusal: string): void {
203
+ console.error(refusal)
204
+ console.error(UNTOUCHED_NOTE)
205
+ }
206
+
207
+ interface PlanOutcome {
208
+ plan?: RunPlan
209
+ refusal?: string
210
+ }
211
+
212
+ // The releases and the consumers for a version already accepted, or the reason there are none.
213
+ function plan_consumers(version: string, options: RunOptions): PlanOutcome {
214
+ const { plan: releases, refusal } = propagate_plan.build_plan(PROJECT_ROOT, version)
215
+ if (releases === undefined) return { refusal: refusal ?? UNTOUCHED_NOTE }
216
+ const selection = resolve_run_targets(releases, version, options.target)
217
+ if (selection.refusal !== undefined) return { refusal: selection.refusal }
218
+
219
+ return { plan: { version, releases, targets: selection.targets } }
220
+ }
221
+
222
+ // The version, the releases and the consumers, or nothing once whatever stopped the run has been
223
+ // printed.
194
224
  function plan_run(options: RunOptions): RunPlan | undefined {
195
225
  const version = announce_run_version(resolve_run_version(PROJECT_ROOT, options.is_dry_run))
196
226
  if (version === undefined) return undefined
197
- const selection = resolve_run_targets(version, options.target)
227
+ const { plan, refusal } = plan_consumers(version, options)
228
+ if (refusal !== undefined) refuse_run(refusal)
198
229
 
199
- if (selection.refusal !== undefined) {
200
- console.error(selection.refusal)
201
- console.error(UNTOUCHED_NOTE)
202
-
203
- return undefined
204
- }
205
-
206
- return { version, targets: selection.targets }
230
+ return plan
207
231
  }
208
232
 
209
233
  // Everything after the publish wait: run each consumer, print one report.
210
234
  function propagate_to_consumers(plan: RunPlan, is_dry_run: boolean): number {
211
- const { version, targets } = plan
235
+ const { releases, targets } = plan
212
236
  const step = is_dry_run
213
- ? propagate_steps.describe_step
214
- : propagate_steps.create_step_runner({
215
- releases: [
216
- {
217
- package_name: KIT_PACKAGE_NAME,
218
- version,
219
- bin_name: propagate_steps.JOSH_BIN,
220
- },
221
- ],
222
- origin: propagate_steps.PROPAGATE_ORIGIN,
223
- })
237
+ ? propagate_plan.create_step_describer(releases)
238
+ : propagate_steps.create_step_runner(releases)
224
239
  const results = propagate_run.run_targets(
225
240
  targets,
226
241
  step,
@@ -235,8 +250,12 @@ function propagate_to_consumers(plan: RunPlan, is_dry_run: boolean): number {
235
250
 
236
251
  // A dry run never waits: the whole reason to ask for one is to see the target list, and a wait that
237
252
  // times out would end the run before the list was ever printed.
238
- async function resolve_publish(version: string, options: RunOptions): Promise<boolean> {
239
- return await await_publish(version, options.is_dry_run || options.is_publish_wait_skipped)
253
+ async function resolve_publish(plan: RunPlan, options: RunOptions): Promise<boolean> {
254
+ return await await_publish(
255
+ propagate_plan.supplier_name(plan.releases),
256
+ plan.version,
257
+ options.is_dry_run || options.is_publish_wait_skipped,
258
+ )
240
259
  }
241
260
 
242
261
  async function run(argv: ReadonlyArray<string>): Promise<number> {
@@ -250,7 +269,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
250
269
 
251
270
  const plan = plan_run(options)
252
271
  if (plan === undefined) return FAILURE_EXIT_CODE
253
- if (!(await resolve_publish(plan.version, options))) return FAILURE_EXIT_CODE
272
+ if (!(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
254
273
 
255
274
  return propagate_to_consumers(plan, options.is_dry_run)
256
275
  }
@@ -1,5 +1,5 @@
1
- import { readFileSync } from 'node:fs'
2
1
  import path from 'node:path'
2
+ import { file_reader } from '#scripts/lib/read-file'
3
3
 
4
4
  // The scope expansion `prompts/refactoring.md` §4.3 used to describe in prose (joshuafolkken/kit#2180):
5
5
  // from the seed files, follow imports both ways — the files a seed imports and the files that import a
@@ -67,11 +67,7 @@ function resolve_spec(
67
67
  }
68
68
 
69
69
  function read_source(file: string): string {
70
- try {
71
- return readFileSync(file, 'utf8')
72
- } catch {
73
- return ''
74
- }
70
+ return file_reader.read_if_readable(file) ?? ''
75
71
  }
76
72
 
77
73
  function file_imports(file: string, root: string, known: ReadonlySet<string>): ReadonlySet<string> {
@@ -219,6 +219,7 @@ function plan(project_npmrc: string, user_npmrc: string, lockfile: string): Migr
219
219
  const migrate_logic = {
220
220
  plan,
221
221
  github_tarballs,
222
+ is_github_tarball,
222
223
  rewrite_kit_lockfile,
223
224
  registry_of,
224
225
  scoped_packages,
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, rmSync, 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 { execa } from 'execa'
5
6
  import { z } from 'zod'
6
7
  import { migrate_logic, type MigrationPlan } from './migrate-logic'
@@ -70,7 +71,7 @@ interface MigrationContext {
70
71
  }
71
72
 
72
73
  function read_file(file_path: string): string {
73
- return existsSync(file_path) ? readFileSync(file_path, 'utf8') : ''
74
+ return file_reader.read_file_or_empty(file_path)
74
75
  }
75
76
 
76
77
  function has_registry_override(environment: NodeJS.ProcessEnv): boolean {
@@ -1,13 +1,13 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
1
  import os from 'node:os'
3
2
  import path from 'node:path'
3
+ import { file_reader } from '#scripts/lib/read-file'
4
4
 
5
5
  const NPMRC = '.npmrc'
6
6
 
7
7
  function read(home: string = os.homedir()): string {
8
8
  const file = path.join(home, NPMRC)
9
9
 
10
- return existsSync(file) ? readFileSync(file, 'utf8') : ''
10
+ return file_reader.read_file_or_empty(file)
11
11
  }
12
12
 
13
13
  const user_npmrc = { read }
@@ -4,7 +4,7 @@ import { z } from 'zod'
4
4
 
5
5
  const API_URL = 'https://api.github.com/repos/joshuafolkken/kit/releases'
6
6
  const WORKFLOW_RUNS_URL =
7
- 'https://api.github.com/repos/joshuafolkken/kit/actions/workflows/publish.yml/runs?event=repository_dispatch&per_page=100'
7
+ 'https://api.github.com/repos/joshuafolkken/kit/actions/workflows/publish.yml/runs?per_page=100'
8
8
  const WORKFLOW_JOBS_URL = 'https://api.github.com/repos/joshuafolkken/kit/actions/runs'
9
9
  const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:[.-][0-9A-Za-z.-]+)?$/u
10
10
  const REQUEST_TIMEOUT_MS = 30_000
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { git_command } from '#scripts/git/git-command'
4
+ import { error_text } from '#scripts/lib/error-message'
4
5
  import { release_history } from './release-history'
5
6
  import { release_plan, type ReleasePlan } from './release-plan'
6
7
  import { release_publish } from './release-publish'
@@ -76,7 +77,7 @@ async function main(): Promise<void> {
76
77
  try {
77
78
  process.exit(await run(is_dry_run_requested(process.argv)))
78
79
  } catch (error) {
79
- console.error(error instanceof Error ? error.message : String(error))
80
+ console.error(error_text.message_of(error))
80
81
  process.exit(FAILURE_EXIT_CODE)
81
82
  }
82
83
  }
@@ -1,7 +1,5 @@
1
1
  import { git_gh_exec } from '#scripts/git/git-gh-exec'
2
2
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
3
- import { read_spawn_stdout } from '#scripts/lib/spawn-exit'
4
- import { execaSync } from 'execa'
5
3
 
6
4
  // The shared half of every "report a GitHub repository setting kit cannot write" check.
7
5
  //
@@ -27,17 +25,21 @@ interface RepoApiResult {
27
25
  stdout: string
28
26
  }
29
27
 
28
+ const FAILED_EXIT_CODE = 1
29
+
30
30
  // One `gh api` call. Never throws and never propagates a non-zero exit: an unreadable answer is a
31
- // reported status, not a command failure.
31
+ // reported status, not a command failure. Every failure reads as `FAILED_EXIT_CODE` with no output,
32
+ // which `parse_payload` treats as unreadable whatever gh's own exit code was.
32
33
  function query_repo_api(api_path: string): RepoApiResult {
33
- const result = execaSync('gh', ['api', api_path], {
34
- ...git_gh_exec.direct_environment(),
34
+ const stdout = git_gh_exec.read_gh_api_sync({
35
+ path: api_path,
35
36
  cwd: PROJECT_ROOT,
36
- reject: false,
37
- timeout: GH_TIMEOUT_MS,
37
+ timeout_ms: GH_TIMEOUT_MS,
38
38
  })
39
39
 
40
- return { exit_code: result.exitCode ?? 1, stdout: read_spawn_stdout(result) }
40
+ return stdout === undefined
41
+ ? { exit_code: FAILED_EXIT_CODE, stdout: '' }
42
+ : { exit_code: 0, stdout }
41
43
  }
42
44
 
43
45
  // The response body as an object, or nothing. A non-zero exit covers every case where the answer
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto'
2
+ import { lstatSync } from 'node:fs'
2
3
  import { git_command } from '#scripts/git/git-command'
3
4
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
4
5
  import { stamp_file } from '#scripts/josh/stamp-file'
@@ -240,6 +241,47 @@ function check(now: number = Date.now(), root?: string): AttestVerdict {
240
241
  return verdict
241
242
  }
242
243
 
244
+ function is_written_since(source: string, taken_at: string): boolean {
245
+ try {
246
+ const stats = lstatSync(source)
247
+
248
+ return stats.isFile() && stats.mtimeMs >= Date.parse(taken_at)
249
+ } catch {
250
+ return false
251
+ }
252
+ }
253
+
254
+ // **Whether the last briefed review of this branch finished** (joshuafolkken/kit#2945). A ship resumed
255
+ // after a stop re-issues round 1, and without this answer it briefed the whole change again — the
256
+ // files the finished review had just read, re-read at full cost. It is asked before `record_target`
257
+ // mints the next nonce, since that call drops the record it reads. The branch must match: the pointer
258
+ // is keyed on the root, and a lane root outlives the branch a previous run reviewed there.
259
+ //
260
+ // **An attestation alone is not a finished review.** The reviewer may attest and then die before it
261
+ // reports — a timeout, a usage limit — and narrowing on that would call files nobody read "already
262
+ // read". So the findings file, written only once the review has its verdict, must postdate the brief
263
+ // this pointer names; one left from an earlier review is older than that brief and does not count.
264
+ function is_reviewed_on(checkout: ReviewCheckout, findings_path: string, root?: string): boolean {
265
+ const pointer = read_pointer(root)
266
+
267
+ if (pointer === undefined) return false
268
+
269
+ const verdict = verdict_for(pointer.nonce)
270
+
271
+ if (verdict.status !== 'ok' || verdict.expected?.branch !== checkout.branch) return false
272
+
273
+ return is_written_since(findings_path, pointer.taken_at)
274
+ }
275
+
276
+ // Whether a record written at `written_at` belongs to the brief the pointer names rather than an earlier
277
+ // one (joshuafolkken/kit#2945): that brief writes it right after minting the pointer. ISO timestamps
278
+ // order as strings.
279
+ function is_briefed_since(written_at: string, root?: string): boolean {
280
+ const pointer = read_pointer(root)
281
+
282
+ return pointer !== undefined && written_at >= pointer.taken_at
283
+ }
284
+
243
285
  // The pointer is keyed on the repository root rather than on `process.cwd()`, which is what
244
286
  // `PROJECT_ROOT` is. `josh review:brief` records against the root it just asked git for, so the check
245
287
  // has to ask the same question: run the two from different directories — one from `scripts/`, one
@@ -303,6 +345,8 @@ const review_attest = {
303
345
  clear_here,
304
346
  EXPECT_PREFIX,
305
347
  expect_path,
348
+ is_briefed_since,
349
+ is_reviewed_on,
306
350
  MISMATCH_VERDICT,
307
351
  MISSING_REASON,
308
352
  POINTER_PREFIX,
@@ -6,8 +6,13 @@ import { scoped_green } from '#scripts/gate/scoped-green'
6
6
  import { change_base } from '#scripts/git/change-base'
7
7
  import { changed_paths } from '#scripts/git/changed-paths'
8
8
  import { git_command } from '#scripts/git/git-command'
9
+ import { PROJECT_ROOT } from '#scripts/init/init-paths'
10
+ import type { FileMapStamp } from '#scripts/josh/file-map-stamp'
9
11
  import { find_package_directory } from '#scripts/josh/josh-logic'
10
12
  import { path_decision } from '#scripts/josh/path-decision'
13
+ import { stamp_file } from '#scripts/josh/stamp-file'
14
+ import { openai_review_broker } from '#scripts/lane/openai-review-broker'
15
+ import { run_ship_review } from '#scripts/run/run-ship-review'
11
16
  import { review_attest } from './review-attest'
12
17
  import { review_brief } from './review-brief'
13
18
  import { review_checkout, type ReviewCheckout } from './review-checkout'
@@ -134,13 +139,54 @@ function record_round_one(
134
139
  // failed to record would hand the review a command that cannot succeed and the run a check that
135
140
  // answers `not-required` — the guard gone, silently, which is the shape this whole record exists to
136
141
  // remove.
137
- async function open_contract(): Promise<{ checkout: ReviewCheckout; nonce: string }> {
142
+ //
143
+ // **Whether this is a resume is read first** (joshuafolkken/kit#2945): `record_target` drops the
144
+ // previous nonce's records, so asked afterwards the finished review would always read as absent. The
145
+ // findings file is the ship's, keyed on `PROJECT_ROOT` as the ship keys it; a review run outside the
146
+ // ship writes none, so it never reads as a resume and keeps the whole change.
147
+ //
148
+ // **The narrowing is measured from the tree that review was briefed on**, and only when that record
149
+ // was written by the brief the pointer names: one left by an earlier brief is not the tree the
150
+ // finished review read, so the resume reads the whole change instead.
151
+ function attested_tree(checkout: ReviewCheckout): FileMapStamp | undefined {
152
+ const { findings } = openai_review_broker.review_paths(PROJECT_ROOT)
153
+ const briefed = review_stamps.briefed_stamp.read()
154
+
155
+ if (briefed === undefined || !review_attest.is_briefed_since(briefed.taken_at, checkout.root)) {
156
+ return undefined
157
+ }
158
+
159
+ const is_clean =
160
+ review_attest.is_reviewed_on(checkout, findings, checkout.root) &&
161
+ run_ship_review.is_clean_round_one(stamp_file.read_stamp_text(findings))
162
+
163
+ return is_clean ? briefed : undefined
164
+ }
165
+
166
+ async function open_contract(): Promise<{
167
+ checkout: ReviewCheckout
168
+ nonce: string
169
+ resumed_from: FileMapStamp | undefined
170
+ }> {
138
171
  const checkout = await review_checkout.read_checkout()
172
+ const resumed_from = attested_tree(checkout)
139
173
 
140
174
  // Keyed on the root git just answered with, never on `process.cwd()`: the check made before the
141
175
  // merge asks git the same question, and the two hash different keys the moment one of them runs
142
176
  // from a subdirectory — a mismatch that would drop the guard with nothing printed.
143
- return { checkout, nonce: review_attest.record_target(checkout, checkout.root) }
177
+ return { checkout, nonce: review_attest.record_target(checkout, checkout.root), resumed_from }
178
+ }
179
+
180
+ // Retaken on every round-1 brief, after the pointer it pairs with (joshuafolkken/kit#2945). Swallowed:
181
+ // a record that failed to write predates the next pointer, so the resume widens rather than narrows.
182
+ function record_briefed(round: number, tree: Record<string, string>, base?: string): void {
183
+ if (round !== FIRST_ROUND) return
184
+
185
+ try {
186
+ review_stamps.briefed_stamp.write(tree, undefined, base)
187
+ } catch {
188
+ /* no record widens the resume rather than narrowing it */
189
+ }
144
190
  }
145
191
 
146
192
  interface ComposeRequest {
@@ -252,6 +298,7 @@ async function run_review(round: number, reading: ChangeReading): Promise<number
252
298
  if (resolved.kind === 'rejected') return report_error(resolved.note)
253
299
  console.info(await compose_brief({ round, paths, tree, base, profile: resolved.profile }))
254
300
  record_round_one(round, tree, commit)
301
+ record_briefed(round, tree, commit)
255
302
 
256
303
  return 0
257
304
  }
@@ -357,6 +357,39 @@ function round_two_block(
357
357
  return narrow_block(scope.recorded_at, scope, root, base)
358
358
  }
359
359
 
360
+ // **A resumed round 1 reads what changed since the attested review, reusing round 2's reconciliation**
361
+ // (joshuafolkken/kit#2945). A ship that stopped and was re-issued briefed the whole change again, so
362
+ // the reviewer re-read every file the attested review had just read. The scope is the comparison round
363
+ // 2 makes, measured from the tree that review was briefed on. Every reading that cannot narrow falls back to the whole
364
+ // change: no record, a moved base, or an empty target. An empty target is included because a resumed
365
+ // review that read nothing would report a clean verdict over the one already recorded.
366
+ const RESUMED_HEADING =
367
+ 'Resumed round 1 — a review of this branch was already attested, so this pass reads only what changed since the tree it read.'
368
+
369
+ const RESUMED_QUESTION =
370
+ 'Review these files in full against the rubric — they are the only code this pass has not already read.'
371
+
372
+ function resumed_target(
373
+ snapshot: FileMapStamp | undefined,
374
+ tree: Record<string, string>,
375
+ root: string,
376
+ base: string,
377
+ ): string {
378
+ const scope = round_two_scope(snapshot, tree, base)
379
+
380
+ if (scope.recorded_at === undefined || scope.target.length === 0) {
381
+ return whole_change_target(root, base)
382
+ }
383
+
384
+ return [
385
+ RESUMED_HEADING,
386
+ `The attested review read the tree recorded at ${scope.recorded_at}.`,
387
+ `Target: only these files, which changed since that tree:\n${format_paths(root, scope.target)}`,
388
+ ...reconciliation(root, base, scope),
389
+ RESUMED_QUESTION,
390
+ ].join('\n')
391
+ }
392
+
360
393
  interface BriefStamps {
361
394
  gate: FileMapStamp | undefined
362
395
  in_flight: FileMapStamp | undefined
@@ -377,12 +410,23 @@ interface BriefInput {
377
410
  // The rubric's absolute path, resolved by the caller against the kit package rather than the review
378
411
  // checkout, so it resolves in a consumer that ships no `prompts/` of its own (joshuafolkken/kit#2402).
379
412
  rubric_path: string
413
+ // The tree a finished, clean review of this branch was briefed on, so round 1 is a resume
414
+ // (joshuafolkken/kit#2945). Absent, round 1 reads the whole change.
415
+ resumed_from?: FileMapStamp | undefined
380
416
  }
381
417
 
382
418
  const SECOND_ROUND = 2
383
419
 
420
+ function round_one_target(input: BriefInput): string {
421
+ const { root } = input.checkout
422
+
423
+ if (input.resumed_from === undefined) return whole_change_target(root, input.base)
424
+
425
+ return resumed_target(input.resumed_from, input.tree, root, input.base)
426
+ }
427
+
384
428
  function target_block(input: BriefInput): string {
385
- if (input.round < SECOND_ROUND) return whole_change_target(input.checkout.root, input.base)
429
+ if (input.round < SECOND_ROUND) return round_one_target(input)
386
430
 
387
431
  return round_two_block(input.stamps.round_one, input.tree, input.checkout.root, input.base)
388
432
  }
@@ -422,6 +466,7 @@ const review_brief = {
422
466
  green_stamp,
423
467
  in_flight_line,
424
468
  live_marker,
469
+ RESUMED_HEADING,
425
470
  matching_stamp,
426
471
  no_snapshot_line,
427
472
  NOTHING_LEFT_LINE,
@@ -1,9 +1,9 @@
1
1
  // The review-finding line's grammar, its category vocabulary and the aggregation over it — the single
2
2
  // place all three are defined (joshuafolkken/kit#2325). A `/code-review` round's findings evaporated
3
3
  // the moment they were fixed, so no one could say which category recurred. They now land in the same
4
- // append-only ledger the observation lines use (`docs/maintainers/observations.md`), under a distinct `- rf:`
5
- // prefix so `observation-ledger-line.ts`'s `- k:` grammar never treats them as its own — one ledger
6
- // file, one flush path, a second line *type* rather than a second ledger.
4
+ // append-only ledger the observation lines use (`docs/maintainers/observations/`), under a distinct `- rf:`
5
+ // prefix so `observation-ledger-line.ts`'s `- k:` grammar never treats them as its own — one ledger,
6
+ // one commit path, a second line *type* rather than a second ledger.
7
7
 
8
8
  const FINDING_PREFIX = '- rf:'
9
9
  const FIELD_SEPARATOR = ' | '