@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
@@ -0,0 +1,144 @@
1
+ import { CONTEXT_CUT_THRESHOLD } from '#scripts/cost-runtime/context-cut-threshold'
2
+ import { cost_cli, type CostVerdict } from '#scripts/cost-runtime/cost-cli'
3
+ import { cost_format } from '#scripts/cost-runtime/cost-format'
4
+ import { cost_verdict } from '#scripts/cost-runtime/cost-verdict'
5
+ import { hook_decision } from '#scripts/josh/hook-decision'
6
+ import { implementation_cut_verdict } from '#scripts/rules/implementation-cut-verdict'
7
+ import { run_carry, type RunCarry } from './run-carry'
8
+ import { run_headless } from './run-headless'
9
+ import { run_watcher_hook } from './run-watcher-hook'
10
+
11
+ // The `backlogrun` parent's hand-off, delivered at the call it binds on (joshuafolkken/kit#2947).
12
+ //
13
+ // **The parent's hand-off was asked only at a merge, and the context grew between merges.** The
14
+ // orchestrator never implements, yet on 2026-10-02 parent 259fb6d5 sent 111 of its 170 requests past
15
+ // the shared threshold, peaking at 207,081 tokens: `pnpm josh cost --cut` is read after each child's
16
+ // merge (`backlogrun-progress.md` → "The hand-off"), and a long wait or a lane-failure investigation
17
+ // between two merges is exactly where it went unread. This is the lesson `implementation-cut.ts` records
18
+ // for a lane child, one session up — a check carried as a step is taken late, a `PreToolUse` refusal is
19
+ // taken at the next call.
20
+ //
21
+ // **The same statistic and the same threshold, never a second measurement.** The verdict is
22
+ // `cost_cli.session_verdict` — what `pnpm josh cost --cut` prints — read through the implementation cut's
23
+ // short reuse window so a burst of calls is priced once.
24
+ //
25
+ // **It is async and composed after the synchronous guards, like the watcher guard.** Telling the parent
26
+ // apart reads the carry record through `run_headless.driving_carry`, which is asynchronous; reusing it is
27
+ // what keeps a second, synchronous spelling of the parent test from existing.
28
+
29
+ const SWITCH_ENV_KEY = 'JOSH_PARENT_CUT_GUARD'
30
+ const STAMP_PREFIX = 'josh-parent-cut-guard-'
31
+
32
+ // **One refusal, then a quiet window long enough to finish the hand-off in.** The hand-off is several
33
+ // calls — `lane:list`, `lane:output`, `run:carry --cut`, `run:wake`, the progress comment — and a guard
34
+ // refusing each of them would wedge the very procedure it asks for. A parent that could not cut (a lane
35
+ // nobody can poll) is asked again once the window has passed, which is the "next opportunity" the merge
36
+ // seam used to be.
37
+ const QUIET_WINDOW_MS = 600_000
38
+
39
+ // What the decision has to know about the world, passed in so it is testable without a carry record or
40
+ // a session transcript on disk.
41
+ interface ParentCutState {
42
+ carry: () => Promise<RunCarry | undefined>
43
+ verdict: () => CostVerdict
44
+ }
45
+
46
+ function current_state(): ParentCutState {
47
+ return {
48
+ carry: async (): Promise<RunCarry | undefined> => await run_headless.driving_carry(),
49
+ verdict: (): CostVerdict =>
50
+ implementation_cut_verdict.reused_verdict(() => cost_cli.session_verdict()),
51
+ }
52
+ }
53
+
54
+ const THRESHOLD_TEXT = `${cost_format.format_tokens(CONTEXT_CUT_THRESHOLD)}-token`
55
+
56
+ const PARENT_CUT_REASON =
57
+ '⛔ backlogrun parent hand-off: this session drives a `backlogrun` (it owns the live carry record) and ' +
58
+ `its recent-context cost has crossed the shared ${THRESHOLD_TEXT} threshold between merges, so every ` +
59
+ 'later request re-reads the accumulated context (joshuafolkken/kit#2947). Take the hand-off now rather ' +
60
+ 'than at the next merge: follow `.claude/skills/workflow-commands/backlogrun-progress.md` → "The ' +
61
+ 'hand-off" from its `over` branch — open no new lane and take no new child, read `pnpm josh lane:list`, ' +
62
+ 'make sure every lane in flight records an output path, then run `pnpm josh run:carry --cut --owner ' +
63
+ '"$PPID"` and, unless it answers `capped`, `pnpm josh run:wake`, post the progress comment and end the ' +
64
+ 'turn. A lane nobody can poll means the cut does not happen; carry on and this asks again later. ' +
65
+ 'Reissue this call when the hand-off is done or cannot be taken — the guard stays quiet for a while ' +
66
+ 'after it refuses, so the hand-off calls themselves are not refused.'
67
+
68
+ // Neither an uncapped parent's own record nor a measured `over` is optional: a capped record has no cut
69
+ // left to take (`run:carry --cut` answers `capped`), and an unmeasurable session is left alone here —
70
+ // refusing every call of a session that cannot be priced would wedge it, while the merge-seam check
71
+ // still takes `over`'s branch for it.
72
+ async function is_due(state: ParentCutState): Promise<boolean> {
73
+ const carry = await state.carry()
74
+
75
+ if (carry === undefined || run_carry.is_at_cut_cap(carry)) return false
76
+
77
+ return state.verdict() === cost_verdict.OVER_VERDICT
78
+ }
79
+
80
+ function is_quiet(target: string, now_ms: number): boolean {
81
+ const last_ms = hook_decision.create_refusal_stamp(STAMP_PREFIX).last_ms(target)
82
+
83
+ return last_ms !== hook_decision.NEVER_MS && now_ms - last_ms < QUIET_WINDOW_MS
84
+ }
85
+
86
+ // The quiet window is read before the carry record and the transcript price, so a refused parent's
87
+ // following calls cost one small file read each.
88
+ async function refusal_for(
89
+ transcript: string,
90
+ now_ms: number,
91
+ state: ParentCutState,
92
+ ): Promise<string | undefined> {
93
+ const stamp = hook_decision.create_refusal_stamp(STAMP_PREFIX)
94
+ const target = stamp.path(transcript)
95
+
96
+ if (is_quiet(target, now_ms) || !(await is_due(state))) return undefined
97
+
98
+ return stamp.record(target, now_ms) ? PARENT_CUT_REASON : undefined
99
+ }
100
+
101
+ // A delegated unit's call names the *parent's* transcript and runs under the parent's process, so the
102
+ // carry record reads as owned here; only `agent_id` tells it apart (`hook-decision.ts`). Refusing it
103
+ // would hand the unit a hand-off it cannot take and spend the parent's quiet window on it.
104
+ function is_unit_call(raw_payload: string): boolean {
105
+ try {
106
+ return (hook_decision.parse_hook_payload(raw_payload)?.agent_id ?? '') !== ''
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ function is_exempt(raw_payload: string): boolean {
113
+ return !hook_decision.is_switch_enabled(SWITCH_ENV_KEY) || is_unit_call(raw_payload)
114
+ }
115
+
116
+ /**
117
+ * The deny reason for a call a `backlogrun` parent makes past the shared threshold, or `undefined` when
118
+ * the call proceeds — a lane child, a delegated unit, a person's session, a handed-off or capped
119
+ * record, an `under` or unmeasurable verdict, or a call inside the quiet window after a refusal.
120
+ */
121
+ async function parent_cut_reason(
122
+ raw_payload: string,
123
+ now_ms: number = Date.now(),
124
+ state: ParentCutState = current_state(),
125
+ ): Promise<string | undefined> {
126
+ if (is_exempt(raw_payload)) return undefined
127
+
128
+ const transcript = run_watcher_hook.transcript_of(raw_payload)
129
+
130
+ if (transcript === undefined) return undefined
131
+
132
+ return await refusal_for(transcript, now_ms, state)
133
+ }
134
+
135
+ const run_parent_cut_hook = {
136
+ PARENT_CUT_REASON,
137
+ QUIET_WINDOW_MS,
138
+ STAMP_PREFIX,
139
+ SWITCH_ENV_KEY,
140
+ parent_cut_reason,
141
+ }
142
+
143
+ export type { ParentCutState }
144
+ export { run_parent_cut_hook }
@@ -1,4 +1,4 @@
1
- import { parseArgs } from 'node:util'
1
+ import { cli_flags } from '#scripts/lib/cli-flags'
2
2
  import { run_progress_config } from './run-progress-config'
3
3
 
4
4
  // The argument half of `josh run:progress`, kept apart from the watcher so the CLI stays inside the file
@@ -37,11 +37,7 @@ interface ParsedValues {
37
37
  }
38
38
 
39
39
  function read_arguments(argv: ReadonlyArray<string>): ParsedValues | undefined {
40
- try {
41
- return parseArgs({ args: [...argv], options: OPTIONS }).values
42
- } catch {
43
- return undefined
44
- }
40
+ return cli_flags.parse_or_undefined({ args: [...argv], options: OPTIONS })?.values
45
41
  }
46
42
 
47
43
  // A hand-typed `--interval` outranks the environment, the environment outranks the interval the
@@ -5,6 +5,7 @@ import { backlog_arrival, type ArrivalProbe } from '#scripts/backlog/backlog-arr
5
5
  import { backlog_ready } from '#scripts/backlog/backlog-ready'
6
6
  import { gh_spawn } from '#scripts/gh/gh-spawn'
7
7
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
8
+ import { error_text } from '#scripts/lib/error-message'
8
9
  import { run_event_stream_emit } from './run-event-stream-emit'
9
10
  import { run_progress, type ProgressState } from './run-progress'
10
11
  import { run_progress_args, type ParsedValues } from './run-progress-args'
@@ -229,7 +230,7 @@ function decline(loop: WatchLoop, now_ms: number, notice: string): WatchLoop {
229
230
  // under load — the very load this command exists to report — would otherwise take the reporting down
230
231
  // silently for the rest of an unattended run, with the parent not waiting on it and nothing saying why.
231
232
  function to_notice(error: unknown): string {
232
- return `${FAILED_TICK_PREFIX} ${error instanceof Error ? error.message : String(error)}`
233
+ return `${FAILED_TICK_PREFIX} ${error_text.message_of(error)}`
233
234
  }
234
235
 
235
236
  async function attempt(
@@ -78,7 +78,7 @@ async function to_child(issue: OpenIssueData): Promise<ChildObservation> {
78
78
  const number = String(issue.number)
79
79
  const child = await run_preflight.read_child_state(number)
80
80
 
81
- return { issue: number, labels: to_labels(issue), pr_state: child.pr_state }
81
+ return { issue: number, title: issue.title, labels: to_labels(issue), pr_state: child.pr_state }
82
82
  }
83
83
 
84
84
  async function read_children(repo: string): Promise<ReadonlyArray<ChildObservation> | undefined> {
@@ -171,6 +171,7 @@ async function read_observations(request: ObservationRequest): Promise<Observati
171
171
  if (children.length === 0 && !(await has_run_started(lanes))) return IDLE_READ
172
172
 
173
173
  const observations: Observations = {
174
+ repo: request.repo,
174
175
  children,
175
176
  lanes,
176
177
  load_average: loadavg()[FIRST_LOAD_AVERAGE] ?? 0,
@@ -1,3 +1,5 @@
1
+ import { issue_cite, type IssueCiter } from '#scripts/issue/issue-cite'
2
+
1
3
  // `josh run:progress` — the one line an unattended run prints when it has gone quiet, and the clock
2
4
  // that decides when that is (joshuafolkken/kit#1520).
3
5
  //
@@ -56,6 +58,9 @@ const DEFAULT_INTERVAL_MS = DEFAULT_INTERVAL_MINUTES * MS_PER_MINUTE
56
58
  // a second spelling of it — `none` / `open` / `merged` / `closed`.
57
59
  interface ChildObservation {
58
60
  issue: string
61
+ // Kept from the `in-progress` listing the child was read from, so the line cites it in full
62
+ // (joshuafolkken/kit#2943) without a read per child.
63
+ title: string
59
64
  labels: ReadonlyArray<string>
60
65
  pr_state: string
61
66
  }
@@ -66,6 +71,8 @@ interface LaneObservation {
66
71
  }
67
72
 
68
73
  interface Observations {
74
+ // `owner/repo`, the repository the listing was read from — what every printed issue links into.
75
+ repo: string
69
76
  children: ReadonlyArray<ChildObservation>
70
77
  lanes: ReadonlyArray<LaneObservation>
71
78
  load_average: number
@@ -195,26 +202,32 @@ function next_state(
195
202
  return { key, unchanged_since_ms: now_ms }
196
203
  }
197
204
 
198
- function format_child(child: ChildObservation): string {
205
+ // The issue leads as a citation rather than a bare `#N` (joshuafolkken/kit#2943): the parent relays these
206
+ // lines verbatim, and a bare number copied into a reply is what the Stop guard sends back.
207
+ function format_child(child: ChildObservation, cite: IssueCiter): string {
199
208
  const labels = child.labels.length > 0 ? child.labels.join(',') : '(no labels)'
200
209
 
201
- return `#${child.issue} ${labels} PR:${child.pr_state}`
210
+ return `${cite(child.issue)} ${labels} PR:${child.pr_state}`
202
211
  }
203
212
 
204
213
  // An empty set is the pre-label stage, said as such rather than left as a blank slot between the
205
214
  // separators (joshuafolkken/kit#1900). After that change a `read_observations` result carries no
206
215
  // children exactly when a run has started and none has reached `in-progress` yet, so this is the one
207
216
  // place the two cases are told apart on the line.
208
- function format_children(children: ReadonlyArray<ChildObservation>): string {
217
+ function format_children(children: ReadonlyArray<ChildObservation>, repo: string): string {
209
218
  if (children.length === 0) return NO_CHILD_YET
219
+ const cite = issue_cite.citer(repo, new Map(children.map((child) => [child.issue, child.title])))
210
220
 
211
- return children.map((child) => format_child(child)).join(' · ')
221
+ return children.map((child) => format_child(child, cite)).join(' · ')
212
222
  }
213
223
 
214
- function format_lanes(lanes: ReadonlyArray<LaneObservation>): string {
224
+ // A lane is linked without its title: a lane in flight is a child the line above already cited in full,
225
+ // so repeating the title here would only lengthen the line.
226
+ function format_lanes(lanes: ReadonlyArray<LaneObservation>, repo: string): string {
215
227
  if (lanes.length === 0) return NO_LANES
228
+ const cite = issue_cite.citer(repo, new Map())
216
229
 
217
- return lanes.map((lane) => `${lane.issue}:${lane.state}`).join(' ')
230
+ return lanes.map((lane) => `${cite(lane.issue)}:${lane.state}`).join(' ')
218
231
  }
219
232
 
220
233
  // Absent is said, never filled in. A run started without a transcript path prints `unread` here, which
@@ -303,8 +316,8 @@ function format_line(observations: Observations, timing: LineTiming): string {
303
316
  observed_at: format_observed_at(timing.now_ms),
304
317
  quiet: format_minutes(timing.now_ms - timing.quiet_since_ms),
305
318
  unchanged: format_minutes(timing.now_ms - timing.unchanged_since_ms),
306
- children: format_children(observations.children),
307
- lanes: format_lanes(observations.lanes),
319
+ children: format_children(observations.children, observations.repo),
320
+ lanes: format_lanes(observations.lanes, observations.repo),
308
321
  load: observations.load_average.toFixed(1),
309
322
  record: format_record(observations.record_age_ms),
310
323
  next: format_next_report(timing.now_ms, timing.interval_ms),
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { parseArgs } from 'node:util'
4
- import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
5
4
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
6
5
  import { run_event_stream } from './run-event-stream'
7
6
  import { run_event_stream_emit } from './run-event-stream-emit'
@@ -9,8 +8,8 @@ import { run_ship, type ShipSection } from './run-ship'
9
8
  import { run_ship_detach } from './run-ship-detach'
10
9
  import { run_ship_probe } from './run-ship-probe'
11
10
  import { run_ship_return } from './run-ship-return'
12
- import { run_ship_review_steps } from './run-ship-review-steps'
13
11
  import { run_ship_stage, type Phase, type ShipState, type Stage } from './run-ship-stage'
12
+ import { run_ship_steps, type ShipArguments, type Step } from './run-ship-steps'
14
13
 
15
14
  // `josh ship "<title> #<N>"` — one call for the fixed commit-to-report region a run ships a change on
16
15
  // (joshuafolkken/kit#2398). The loop used to spend a round trip each on `gate`, `git -y`, `followup`
@@ -67,7 +66,6 @@ const USAGE =
67
66
  const NO_LOG_NOTE = 'no detached ship supervisor log for this issue'
68
67
  const LANE_CHILD_DETACH_NOTE =
69
68
  'lane child: shipping through the detached supervisor (--detach implied), so the end of this turn cannot kill it'
70
- const should_forward_stderr = true
71
69
 
72
70
  type NotifyValues = Partial<Record<(typeof NOTIFY_OPTIONS)[number], string>>
73
71
 
@@ -84,24 +82,8 @@ const OPTIONS = {
84
82
  cite: { type: 'string', multiple: true },
85
83
  } as const
86
84
 
87
- interface ShipArguments {
88
- title: string
89
- number: string
90
- notify: ReadonlyArray<string>
91
- body: ReadonlyArray<string>
92
- cites: ReadonlyArray<string>
93
- is_review: boolean
94
- is_detach: boolean
95
- }
96
-
97
85
  type ShipCommand = { kind: 'ship'; args: ShipArguments } | { kind: 'log'; number: string }
98
86
 
99
- interface Step {
100
- stage: Stage
101
- header: string
102
- run: (args: ShipArguments, state: ShipState) => Promise<JoshResult>
103
- }
104
-
105
87
  // What a resumed ship knows before its first stage: the record's path (absent outside a repository),
106
88
  // the stages it says completed, and the repository's actual state (`run-ship-stage.ts` decides from it).
107
89
  interface ShipContext {
@@ -124,58 +106,7 @@ function body_arguments(path: string | undefined): ReadonlyArray<string> {
124
106
  return path === undefined ? [] : [`--${BODY_FILE_OPTION}`, path]
125
107
  }
126
108
 
127
- const { STAGE, PHASE } = run_ship_stage
128
-
129
- // The four steps in the order a change ships: the gate before the commit, the commit/push/PR before
130
- // the merge, the merge before the report bookkeeping. The commit step carries the `--skip-*` flags a
131
- // resumed ship needs, so an existing commit or push is never made twice.
132
- async function josh(argv: ReadonlyArray<string>): Promise<JoshResult> {
133
- return await josh_command.josh_run(argv, should_forward_stderr)
134
- }
135
-
136
- const COMMIT_STEPS: ReadonlyArray<Step> = [
137
- { stage: STAGE.GATE, header: run_ship.GATE_HEADER, run: async () => await josh(['gate']) },
138
- {
139
- stage: STAGE.COMMIT,
140
- header: run_ship.COMMIT_HEADER,
141
- run: async (args, state) =>
142
- await josh(['git', '-y', ...run_ship_stage.commit_flags(state), ...args.body, args.title]),
143
- },
144
- ]
145
-
146
- const MERGE_STEPS: ReadonlyArray<Step> = [
147
- {
148
- stage: STAGE.FOLLOWUP,
149
- header: run_ship.FOLLOWUP_HEADER,
150
- run: async (args) => await josh(['followup', args.title, ...args.notify]),
151
- },
152
- {
153
- stage: STAGE.REPORT,
154
- header: run_ship.REPORT_HEADER,
155
- run: async (args) => await josh(['run:tail', args.number, ...args.cites]),
156
- },
157
- ]
158
-
159
- // `--review` (joshuafolkken/kit#2427) puts the supervised round-1 review in front of the gate: it
160
- // launches the gate itself, so the gate stage that follows reuses that tree's green record. It also
161
- // puts the round-2 pass between the commit and the followup (joshuafolkken/kit#2489), so the PR opens
162
- // between the rounds and round 2 runs beside CI — a no-op when round 1 left no fix delta.
163
- const REVIEW_STEP: Step = {
164
- stage: STAGE.REVIEW,
165
- header: run_ship.REVIEW_HEADER,
166
- run: async (args) => await run_ship_review_steps.review_stage(args.number),
167
- }
168
- const ROUND_TWO_STEP: Step = {
169
- stage: STAGE.ROUND_TWO,
170
- header: run_ship.ROUND_TWO_HEADER,
171
- run: async (args) => await run_ship_review_steps.round_two_stage(args.number),
172
- }
173
-
174
- function steps(args: ShipArguments): ReadonlyArray<Step> {
175
- if (!args.is_review) return [...COMMIT_STEPS, ...MERGE_STEPS]
176
-
177
- return [REVIEW_STEP, ...COMMIT_STEPS, ROUND_TWO_STEP, ...MERGE_STEPS]
178
- }
109
+ const { PHASE } = run_ship_stage
179
110
 
180
111
  function issue_number(title: string): string | undefined {
181
112
  return TRAILING_ISSUE_PATTERN.exec(title)?.[NUMBER_GROUP]
@@ -211,6 +142,7 @@ function ship_args(
211
142
  number,
212
143
  notify: notify_arguments(values),
213
144
  body: body_arguments(values[BODY_FILE_OPTION]),
145
+ body_path: values[BODY_FILE_OPTION],
214
146
  cites,
215
147
  is_review: values.review === true,
216
148
  is_detach: values.detach === true,
@@ -328,14 +260,14 @@ async function stopped(
328
260
  return sections
329
261
  }
330
262
 
331
- // Run the four in order, stopping at the first that failed: a red gate never reaches the commit, so
263
+ // Run the stages in order, stopping at the first that failed: a red gate never reaches the commit, so
332
264
  // the returned sections end at the failure the report names. A ship that reached the end clears its
333
- // record, so the next ship of the same issue starts from the gate.
265
+ // record, so the next ship of the same issue starts from the preflight.
334
266
  async function ship(args: ShipArguments): Promise<ReadonlyArray<ShipSection>> {
335
267
  const context = await open_context(args)
336
268
  const sections: Array<ShipSection> = []
337
269
 
338
- for (const step of steps(args)) {
270
+ for (const step of run_ship_steps.steps(args)) {
339
271
  const section = await run_stage(step, args, context)
340
272
 
341
273
  sections.push(section)
@@ -0,0 +1,87 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { changed_paths } from '#scripts/git/changed-paths'
3
+ import { git_branch } from '#scripts/git/git-branch'
4
+ import { git_gh_command } from '#scripts/git/git-gh-command'
5
+ import { git_preflight } from '#scripts/git/git-preflight'
6
+ import type { JoshResult } from '#scripts/josh/josh-run'
7
+ import { error_text } from '#scripts/lib/error-message'
8
+ import { live_evidence } from '#scripts/review/live-evidence'
9
+ import { run_ship_scoped } from './run-ship-scoped'
10
+
11
+ // `josh ship`'s first stage (joshuafolkken/kit#2946). A detached ship used to meet the pull request's
12
+ // preconditions only at the commit stage — `git -y`'s preflight — or at the merge — `followup`'s
13
+ // live-evidence refusal — each after the review and the gate had already run, so every such stop
14
+ // relaunched a session to repeat them. **Every precondition decidable before the review is asked here,
15
+ // and all of them are reported at once.** No check is new: the classification, issue and branch answers
16
+ // are `git_preflight.problems_of`, and the evidence answer is `live_evidence.verdict_for` over the same
17
+ // changed paths and the body this ship will deliver — `--body-file` when given, else the open PR's.
18
+ //
19
+ // The scoped pair runs last, met rather than stopped on (`run-ship-scoped.ts`), so the review and the
20
+ // gate start on a tree both checks are green on.
21
+
22
+ const SUCCESS_EXIT_CODE = 0
23
+ const FAILURE_EXIT_CODE = 1
24
+ const EVIDENCE_PROBLEM = `This change touches runtime code, but the PR body has no ${live_evidence.EVIDENCE_HEADING} section: run the acceptance criteria for real and pass the body with --body-file <path>.`
25
+
26
+ interface PreflightRequest {
27
+ title: string
28
+ body_path: string | undefined
29
+ }
30
+
31
+ async function body_text(body_path: string | undefined): Promise<string | undefined> {
32
+ if (body_path !== undefined) return await readFile(body_path, 'utf8')
33
+
34
+ return await git_gh_command.pr_get_body(await git_branch.current())
35
+ }
36
+
37
+ async function evidence_problems(body_path: string | undefined): Promise<Array<string>> {
38
+ const [paths, body] = await Promise.all([
39
+ changed_paths.read_changed_paths(false),
40
+ body_text(body_path),
41
+ ])
42
+
43
+ return live_evidence.verdict_for(paths, body) === 'required' ? [EVIDENCE_PROBLEM] : []
44
+ }
45
+
46
+ function refusal(problems: ReadonlyArray<string>): JoshResult {
47
+ const lines = problems.map((problem) => ` - ${problem}`)
48
+ const head = `${String(problems.length)} pull-request precondition(s) unmet before the review and the gate:`
49
+
50
+ return { code: FAILURE_EXIT_CODE, out: [head, ...lines].join('\n') }
51
+ }
52
+
53
+ // The reads here run in the ship's own process, where the commit stage's ran in a `git -y` child: a
54
+ // throw — a `--body-file` path that does not exist, a git read that failed — would escape `ship()`
55
+ // past the stop a supervised ship hands back on, so it is reported as one more unmet precondition.
56
+ // Each side is caught on its own, so one side's failed read never hides what the other side found.
57
+ async function caught(problems: Promise<Array<string>>): Promise<Array<string>> {
58
+ try {
59
+ return await problems
60
+ } catch (error) {
61
+ return [error_text.message_of(error)]
62
+ }
63
+ }
64
+
65
+ async function asked_problems(request: PreflightRequest): Promise<Array<string>> {
66
+ const [preflight, evidence] = await Promise.all([
67
+ caught(git_preflight.problems_of({ cli_input: request.title, will_open_pr: true })),
68
+ caught(evidence_problems(request.body_path)),
69
+ ])
70
+
71
+ return [...preflight, ...evidence]
72
+ }
73
+
74
+ async function stage(request: PreflightRequest): Promise<JoshResult> {
75
+ const problems = await asked_problems(request)
76
+
77
+ if (problems.length > 0) return refusal(problems)
78
+
79
+ const scoped = await run_ship_scoped.scoped_pair()
80
+
81
+ return scoped.code === SUCCESS_EXIT_CODE ? { code: SUCCESS_EXIT_CODE, out: 'ready' } : scoped
82
+ }
83
+
84
+ const run_ship_preflight = { EVIDENCE_PROBLEM, stage }
85
+
86
+ export type { PreflightRequest }
87
+ export { run_ship_preflight }
@@ -1,7 +1,5 @@
1
1
  import { agent_argv, type AgentArgv } from '#scripts/agent/agent-argv'
2
2
  import { agent_role_profile, type AgentProfile } from '#scripts/agent/agent-role-profile'
3
- import { gate_tree } from '#scripts/gate/gate-tree'
4
- import { scoped_green } from '#scripts/gate/scoped-green'
5
3
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
6
4
  import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
7
5
  import { stamp_file } from '#scripts/josh/stamp-file'
@@ -9,6 +7,7 @@ import { lane_child_marker } from '#scripts/lane/lane-child-marker'
9
7
  import { openai_review_broker } from '#scripts/lane/openai-review-broker'
10
8
  import { detached_launch } from './detached-launch'
11
9
  import { run_ship_review, type RoundOutcome, type ScoredVerdict } from './run-ship-review'
10
+ import { run_ship_scoped, type Phase } from './run-ship-scoped'
12
11
 
13
12
  // The side effects of `josh ship --review` (joshuafolkken/kit#2427): the round-1 review the chain used
14
13
  // to open, launch, join, attest and record across five agent turns, run by the supervisor beside the
@@ -47,7 +46,10 @@ const FIXED_JOIN_NOTE =
47
46
  'the round-1 gate read the tree before the review fixes — drained, not judged; the gate stage re-runs it.'
48
47
  const ROUND_TWO_SKIPPED_NOTE = 'round 2 not due — nothing left to verify; shipping on.'
49
48
 
50
- type Phase = () => Promise<JoshResult>
49
+ // The scoped pair's precondition, met by the supervisor itself rather than stopped on
50
+ // (joshuafolkken/kit#2500), single-sourced with the preflight and gate stages (joshuafolkken/kit#2946).
51
+ const { run_phases, scoped_pair } = run_ship_scoped
52
+
51
53
  type PromptOf = (brief_path: string, findings_path: string) => string
52
54
 
53
55
  function failure(out: string): JoshResult {
@@ -153,17 +155,6 @@ async function record_and_route(
153
155
  return outcome.is_passing ? { code: SUCCESS_EXIT_CODE, out } : failure(out)
154
156
  }
155
157
 
156
- async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
157
- let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
158
-
159
- for (const phase of phases) {
160
- last = await phase()
161
- if (last.code !== SUCCESS_EXIT_CODE) return last
162
- }
163
-
164
- return last
165
- }
166
-
167
158
  // A brief-minting command, then a reviewer handed what it printed.
168
159
  function reviewed(
169
160
  open: ReadonlyArray<string>,
@@ -185,18 +176,6 @@ function reviewed(
185
176
  ]
186
177
  }
187
178
 
188
- // The scoped pair's precondition, met by the supervisor itself rather than stopped on
189
- // (joshuafolkken/kit#2500): only the checks with no green record for this tree run — the same
190
- // question `review:brief` and the local gate refuse on — so a fresh record costs nothing and a stale
191
- // or absent one costs a scoped run instead of a stop and a relaunched session. A check that genuinely
192
- // fails still stops the ship, its output forwarded to the ship log.
193
- async function scoped_pair(): Promise<JoshResult> {
194
- const tree = await gate_tree.read_gate_tree()
195
- const scripts = scoped_green.missing_scripts(tree.files, tree.base)
196
-
197
- return await run_phases(scripts.map((script) => async () => await josh([script])))
198
- }
199
-
200
179
  // Fixes made in place are counted only once the scoped pair is green on the fixed tree. The reviewer is
201
180
  // asked to leave it green; this is the check that it did, and a fix it could not get green is routed as
202
181
  // the finding it still is — recorded, then stopped before the gate and the commit take it in.
@@ -154,6 +154,14 @@ function round_one_outcome(verdict: ScoredVerdict): RoundOutcome {
154
154
  return { is_passing: true, note: verdict.kind === VERDICT.FIXED ? FIXED_NOTE : CLEAN_NOTE }
155
155
  }
156
156
 
157
+ // Whether a findings file records a clean round 1 (joshuafolkken/kit#2945). A resumed round 1 narrows to
158
+ // the delta only past a review that found nothing to fix. A blocking one has a finding in a file the
159
+ // delta may not touch. A fixed one approved its own edits, not the briefed tree the delta is measured
160
+ // from, so a file reverted to that tree would drop out of the delta with its finding back in it.
161
+ function is_clean_round_one(text: string | undefined): boolean {
162
+ return read_verdict(text).kind === VERDICT.CLEAN
163
+ }
164
+
157
165
  // Round 2 is final and reads only, so it passes clean or Low-only alone — a fix it made would itself be
158
166
  // unreviewed.
159
167
  function round_two_outcome(verdict: ScoredVerdict): RoundOutcome {
@@ -166,6 +174,7 @@ const run_ship_review = {
166
174
  FIXED_PREFIX,
167
175
  UNVERIFIED_OUTCOME,
168
176
  VERDICT,
177
+ is_clean_round_one,
169
178
  read_verdict,
170
179
  reviewer_prompt,
171
180
  round_one_outcome,
@@ -0,0 +1,44 @@
1
+ import { gate_tree } from '#scripts/gate/gate-tree'
2
+ import { scoped_green } from '#scripts/gate/scoped-green'
3
+ import { josh_command, type JoshResult } from '#scripts/josh/josh-run'
4
+
5
+ // The scoped pair `josh ship` meets itself rather than stops on (joshuafolkken/kit#2500), shared by
6
+ // every stage that needs a green record for this tree (joshuafolkken/kit#2946): the preflight stage at
7
+ // the start, the review rounds, and the gate stage — a reviewer may have edited the tree after round
8
+ // 1's pair ran, and `josh gate` refuses a tree with no green record.
9
+
10
+ const SUCCESS_EXIT_CODE = 0
11
+ const should_forward_stderr = true
12
+
13
+ type Phase = () => Promise<JoshResult>
14
+
15
+ async function run_phases(phases: ReadonlyArray<Phase>): Promise<JoshResult> {
16
+ let last: JoshResult = { code: SUCCESS_EXIT_CODE, out: '' }
17
+
18
+ for (const phase of phases) {
19
+ last = await phase()
20
+ if (last.code !== SUCCESS_EXIT_CODE) return last
21
+ }
22
+
23
+ return last
24
+ }
25
+
26
+ // Only the checks with no green record for this tree run — the same question `review:brief` and the
27
+ // local gate refuse on — so a fresh record costs nothing and a stale or absent one costs a scoped run
28
+ // instead of a stop and a relaunched session. A check that genuinely fails still stops the ship, its
29
+ // output forwarded to the ship log.
30
+ async function scoped_pair(): Promise<JoshResult> {
31
+ const tree = await gate_tree.read_gate_tree()
32
+ const scripts = scoped_green.missing_scripts(tree.files, tree.base)
33
+
34
+ return await run_phases(
35
+ scripts.map(
36
+ (script) => async () => await josh_command.josh_run([script], should_forward_stderr),
37
+ ),
38
+ )
39
+ }
40
+
41
+ const run_ship_scoped = { run_phases, scoped_pair }
42
+
43
+ export type { Phase }
44
+ export { run_ship_scoped }
@@ -17,6 +17,7 @@ import { z } from 'zod'
17
17
  // re-run on an unchanged tree a reuse rather than a second execution. No quality gate is folded away.
18
18
 
19
19
  const STAGE = {
20
+ PREFLIGHT: 'preflight',
20
21
  REVIEW: 'review',
21
22
  GATE: 'gate',
22
23
  COMMIT: 'commit',
@@ -78,7 +79,7 @@ function mark_done(target: string, stage: Stage): void {
78
79
  stamp_file.replace_stamp(target, { done: [...done] })
79
80
  }
80
81
 
81
- // Removed once the report stage completes, so a later ship of the same issue starts from the gate.
82
+ // Removed once the report stage completes, so a later ship of the same issue starts from the preflight.
82
83
  function clear(target: string): void {
83
84
  stamp_file.remove_stamp(target)
84
85
  }
@@ -90,6 +91,9 @@ function is_shipped(state: ShipState): boolean {
90
91
  }
91
92
 
92
93
  const DONE_BY: Record<Stage, (done: ReadonlySet<string>, state: ShipState) => boolean> = {
94
+ // The preflight (joshuafolkken/kit#2946) asks what the commit stage would refuse on, so a commit is
95
+ // its output as it is the gate's; a resumed commit stage still asks `git -y`'s own preflight.
96
+ [STAGE.PREFLIGHT]: (_done, state) => state.is_merged || state.is_committed,
93
97
  // The `--review` round (joshuafolkken/kit#2427) precedes the commit, so a commit is its output as it
94
98
  // is the gate's. A record alone is not honored: before a commit the tree may have been edited since
95
99
  // the recorded round, and only a commit pins the tree that round read.