@joshuafolkken/kit 1.1455.0 → 1.1514.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 (374) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/dependency-update/SKILL.md +26 -55
  3. package/.claude/skills/epic-commands/SKILL.md +83 -156
  4. package/.claude/skills/epic-commands/epic-bundle.md +51 -79
  5. package/.claude/skills/epic-commands/execution-waves.md +43 -81
  6. package/.claude/skills/verify-ui/SKILL.md +24 -50
  7. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  8. package/.claude/skills/workflow-commands/background-commands.md +31 -46
  9. package/.claude/skills/workflow-commands/backlogrun-child.md +65 -85
  10. package/.claude/skills/workflow-commands/backlogrun-lanes.md +86 -125
  11. package/.claude/skills/workflow-commands/backlogrun-park.md +68 -118
  12. package/.claude/skills/workflow-commands/backlogrun-progress.md +17 -28
  13. package/.claude/skills/workflow-commands/backlogrun-recovery.md +17 -26
  14. package/.claude/skills/workflow-commands/backlogrun-steps.md +35 -52
  15. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  16. package/.claude/skills/workflow-commands/chain-rule.md +31 -40
  17. package/.claude/skills/workflow-commands/delegation.md +12 -8
  18. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  19. package/.claude/skills/workflow-commands/followup.md +55 -77
  20. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  21. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  22. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  23. package/.claude/skills/workflow-commands/into-target.md +1 -2
  24. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  25. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  26. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  27. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  28. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  29. package/.claude/skills/workflow-commands/observation-filing.md +59 -118
  30. package/.claude/skills/workflow-commands/observation-ledger.md +52 -112
  31. package/.claude/skills/workflow-commands/pre-gate-cut.md +88 -142
  32. package/.claude/skills/workflow-commands/prerequisite.md +11 -17
  33. package/.claude/skills/workflow-commands/progress-watcher.md +23 -33
  34. package/.claude/skills/workflow-commands/prrun.md +3 -5
  35. package/.claude/skills/workflow-commands/retrospective.md +35 -54
  36. package/.claude/skills/workflow-commands/split-assessment.md +32 -58
  37. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  38. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -73
  39. package/.codex/hooks.json +6 -6
  40. package/.github/actions/setup-node/action.yml +1 -1
  41. package/.github/workflows/ci.yml +1 -1
  42. package/.github/workflows/publish.yml +2 -2
  43. package/CLAUDE.md +7 -6
  44. package/cspell/index.yaml +7 -0
  45. package/dist/CLAUDE.md +7 -6
  46. package/dist/commands/document-section-cli.js +67 -34
  47. package/dist/hooks/batch-guard.js +6 -6
  48. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  49. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-6TS2JD6D.js} +14 -3
  50. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  51. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-A65Y4YOR.js} +3 -3
  52. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-APUO6U4J.js} +102 -19
  53. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  54. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  55. package/dist/hooks/chunk-JP45SOCJ.js +56 -0
  56. package/dist/hooks/{chunk-OLMO72ON.js → chunk-LAOV7NLM.js} +1 -1
  57. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  58. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-O6GDRNJB.js} +9 -7
  59. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-PF2NZL2A.js} +5 -5
  60. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-PTICX4OL.js} +4 -5
  61. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-RT27QRJW.js} +763 -1515
  62. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-SYOTDYCI.js} +1149 -1095
  63. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-TNUMY6NL.js} +1519 -1332
  64. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  65. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-WK6SMBIC.js} +1932 -784
  66. package/dist/hooks/codex-hook-adapter.js +17 -14
  67. package/dist/hooks/duplicate-read-guard.js +7 -7
  68. package/dist/hooks/format-edited.js +5 -4
  69. package/dist/hooks/inputs.json +302 -247
  70. package/dist/hooks/investigation-guard.js +8 -11
  71. package/dist/hooks/pretool-guard.js +16 -13
  72. package/dist/hooks/session-lang.js +372 -26
  73. package/dist/hooks/stop-guard.js +2593 -189
  74. package/dist/josh.js +2128 -282
  75. package/dist/managed-marker/index.js +1 -2
  76. package/dist/version/index.js +15 -12
  77. package/eslint/base.js +2 -1
  78. package/eslint/rules/unicorn.js +16 -0
  79. package/lefthook/base.yml +5 -3
  80. package/package.json +14 -12
  81. package/prompts/coding-standards.md +12 -22
  82. package/prompts/collaboration-workflow/file-edits.md +25 -45
  83. package/prompts/collaboration-workflow/gh-rest.md +19 -36
  84. package/prompts/collaboration-workflow/glossary.md +5 -7
  85. package/prompts/collaboration-workflow/issue-citation.md +6 -14
  86. package/prompts/collaboration-workflow/issue-template.md +40 -32
  87. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  88. package/prompts/collaboration-workflow/operating-rules.md +33 -46
  89. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  90. package/prompts/collaboration-workflow/overview.md +14 -19
  91. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  92. package/prompts/collaboration-workflow/principles.md +57 -54
  93. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  94. package/prompts/collaboration-workflow/report-format.md +41 -49
  95. package/prompts/collaboration-workflow/residency.md +15 -11
  96. package/prompts/collaboration-workflow/rule-delivery.md +7 -15
  97. package/prompts/collaboration-workflow/shell-body.md +10 -20
  98. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  99. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  100. package/prompts/collaboration-workflow/wip-cap.md +26 -32
  101. package/prompts/collaboration-workflow.md +25 -25
  102. package/prompts/refactoring.md +33 -43
  103. package/prompts/review-rubric.md +84 -129
  104. package/prompts/review.md +110 -141
  105. package/prompts/sonar-hotspot-handling.md +22 -50
  106. package/prompts/testing-guide.md +48 -99
  107. package/scripts/adopt/adopt-toolkits.ts +2 -4
  108. package/scripts/agent/agent-argv.ts +20 -4
  109. package/scripts/agent/agent-exit-record.ts +23 -1
  110. package/scripts/agent/agent-role-profile.ts +101 -45
  111. package/scripts/agent/agent-safe-text.ts +60 -0
  112. package/scripts/agent/claude-agent-argv.ts +14 -0
  113. package/scripts/agent/codex-agent-argv.ts +9 -2
  114. package/scripts/agent/lane-instructions.ts +144 -0
  115. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  116. package/scripts/backlog/backlog-budget.ts +4 -4
  117. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  118. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  119. package/scripts/backlog/backlog-drive-named.ts +3 -5
  120. package/scripts/backlog/backlog-drive-restore.ts +28 -2
  121. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  122. package/scripts/backlog/backlog-drive.ts +11 -6
  123. package/scripts/backlog/backlog-occupied.ts +56 -0
  124. package/scripts/backlog/backlog-ready.ts +59 -15
  125. package/scripts/behavior/behavior-rules.ts +3 -2
  126. package/scripts/cost-runtime/codex-usage.ts +4 -2
  127. package/scripts/cost-runtime/cost-cli.ts +1 -2
  128. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  129. package/scripts/delegation/investigation-guard.ts +15 -65
  130. package/scripts/delegation/investigation-reads.ts +1 -2
  131. package/scripts/discovery/repo-discovery.ts +4 -2
  132. package/scripts/discovery/repo-origin.ts +2 -4
  133. package/scripts/doctor/doctor-logic.ts +6 -3
  134. package/scripts/document/document-byte-budget.ts +28 -27
  135. package/scripts/document/document-byte-check.ts +1 -2
  136. package/scripts/document/document-section-cli.ts +17 -5
  137. package/scripts/document/entry-read-budget.ts +3 -3
  138. package/scripts/document/read-set-cli.ts +4 -1
  139. package/scripts/epic/epic-audit-orphans.ts +5 -8
  140. package/scripts/epic/epic-audit-rationale.ts +8 -2
  141. package/scripts/epic/epic-audit.ts +52 -1
  142. package/scripts/epic/epic-bundle-cli.ts +1 -2
  143. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  144. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  145. package/scripts/epic/epic-bundle.ts +17 -9
  146. package/scripts/epic/epic-chains.ts +6 -2
  147. package/scripts/epic/epic-close-comment.ts +1 -2
  148. package/scripts/followup/git-followup-workflow.ts +19 -1
  149. package/scripts/gate/check-inputs.ts +129 -0
  150. package/scripts/gate/check-reuse.ts +162 -0
  151. package/scripts/gate/core-budget.ts +93 -44
  152. package/scripts/gate/core-wait.ts +130 -0
  153. package/scripts/gate/free-cores.ts +49 -0
  154. package/scripts/gate/gate-ledger.ts +17 -3
  155. package/scripts/gate/gate-plan.ts +23 -1
  156. package/scripts/gate/gate-report.ts +15 -2
  157. package/scripts/gate/gate-run-steps.ts +150 -0
  158. package/scripts/gate/held-mark.ts +55 -0
  159. package/scripts/gate/machine-capacity.ts +70 -11
  160. package/scripts/gate/pre-commit-type-check.ts +14 -1
  161. package/scripts/gate/verification-gate.ts +65 -127
  162. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  163. package/scripts/gh/git-gh-issue-write.ts +24 -2
  164. package/scripts/gh/git-pr.ts +19 -3
  165. package/scripts/git/git-common-directory.ts +3 -3
  166. package/scripts/git/git-diff-reads.ts +16 -9
  167. package/scripts/git/git-execa-error.ts +1 -2
  168. package/scripts/git/main-merge-regenerate.ts +78 -0
  169. package/scripts/git/main-merge.ts +14 -5
  170. package/scripts/hooks/batch-guard.ts +5 -8
  171. package/scripts/hooks/format-edited-file.ts +10 -4
  172. package/scripts/hooks/pre-commit-lint.ts +91 -0
  173. package/scripts/hooks/pre-push-unit.ts +15 -4
  174. package/scripts/hooks/pretool-guard.ts +48 -79
  175. package/scripts/hooks/reserved-run.ts +1 -0
  176. package/scripts/hooks/run-hook.sh +10 -12
  177. package/scripts/hooks/step-zero-notice.ts +3 -2
  178. package/scripts/hooks/stop-actions.ts +204 -0
  179. package/scripts/hooks/stop-guard.ts +62 -21
  180. package/scripts/init/hook-command-rewrite.ts +6 -2
  181. package/scripts/init/hook-launch.ts +12 -7
  182. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  183. package/scripts/issue/behavior-change-lint.ts +12 -6
  184. package/scripts/issue/expected-effect-lint.ts +59 -0
  185. package/scripts/issue/issue-cite-cli.ts +35 -15
  186. package/scripts/issue/issue-classification.ts +1 -1
  187. package/scripts/issue/issue-lint-cli.ts +5 -3
  188. package/scripts/issue/issue-scout-cli.ts +22 -16
  189. package/scripts/issue/markdown-section.ts +19 -5
  190. package/scripts/josh/command-usage.ts +110 -0
  191. package/scripts/josh/file-map-stamp.ts +1 -3
  192. package/scripts/josh/hook-decision.ts +17 -2
  193. package/scripts/josh/josh-command-map.ts +2 -0
  194. package/scripts/josh/josh-command-types.ts +55 -9
  195. package/scripts/josh/josh-commands-ai.ts +13 -0
  196. package/scripts/josh/josh-commands-development.ts +1 -1
  197. package/scripts/josh/josh-commands-hooks.ts +6 -0
  198. package/scripts/josh/josh-commands-lane.ts +10 -1
  199. package/scripts/josh/josh-commands-workflow.ts +5 -0
  200. package/scripts/josh/josh-logic.ts +6 -15
  201. package/scripts/josh/josh-usage-automation.ts +121 -0
  202. package/scripts/josh/josh-usage-backlog.ts +180 -0
  203. package/scripts/josh/josh-usage-developer.ts +230 -0
  204. package/scripts/josh/josh-usage-hooks.ts +38 -0
  205. package/scripts/josh/josh-usage-issue.ts +205 -0
  206. package/scripts/josh/josh-usage-review.ts +101 -0
  207. package/scripts/josh/josh-usage-run.ts +252 -0
  208. package/scripts/josh/josh-usage-session.ts +238 -0
  209. package/scripts/josh/josh-usage-workflow.ts +212 -0
  210. package/scripts/josh/josh.ts +14 -5
  211. package/scripts/josh/package-directory.ts +20 -0
  212. package/scripts/lane/lane-await.ts +3 -2
  213. package/scripts/lane/lane-capacity.ts +8 -2
  214. package/scripts/lane/lane-check-stats.ts +56 -0
  215. package/scripts/lane/lane-child-invocation.ts +49 -5
  216. package/scripts/lane/lane-dispatch.ts +3 -3
  217. package/scripts/lane/lane-guard-policy.ts +5 -5
  218. package/scripts/lane/lane-handoff.ts +8 -1
  219. package/scripts/lane/lane-install.ts +12 -4
  220. package/scripts/lane/lane-ledger.ts +52 -1
  221. package/scripts/lane/lane-limit-cli.ts +8 -9
  222. package/scripts/lane/lane-load.ts +9 -2
  223. package/scripts/lane/lane-measure-cli.ts +4 -0
  224. package/scripts/lane/lane-parked.ts +18 -0
  225. package/scripts/lane/lane-preserve.ts +29 -1
  226. package/scripts/lane/lane-process-load.ts +125 -0
  227. package/scripts/lane/lane-process-stats.ts +93 -0
  228. package/scripts/lane/lane-registry.ts +17 -10
  229. package/scripts/lane/lane-restart.ts +5 -26
  230. package/scripts/lane/lane-session-action.ts +146 -0
  231. package/scripts/lane/lane-session-broker.ts +68 -125
  232. package/scripts/lane/lane-spent.ts +14 -3
  233. package/scripts/lane/lane-stats.ts +1 -0
  234. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  235. package/scripts/lane/lane-withdraw.ts +182 -0
  236. package/scripts/lint/format.ts +4 -4
  237. package/scripts/lint/lint-parallel.ts +10 -3
  238. package/scripts/lint/lint-related-scope.ts +4 -1
  239. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  240. package/scripts/notify/telegram-test.ts +10 -5
  241. package/scripts/overrides/lockfile-overrides.ts +1 -3
  242. package/scripts/propagate/propagate.ts +1 -2
  243. package/scripts/release/release-progress.ts +3 -2
  244. package/scripts/report/report-format-reference.ts +2 -0
  245. package/scripts/report/report-lint.ts +7 -4
  246. package/scripts/review/acceptance-map-check.ts +61 -0
  247. package/scripts/review/acceptance-map.ts +205 -0
  248. package/scripts/review/review-brief-cli.ts +1 -1
  249. package/scripts/review/review-record-cli.ts +6 -2
  250. package/scripts/rules/agent-model.ts +59 -0
  251. package/scripts/rules/delivered-rules.ts +11 -2
  252. package/scripts/rules/destructive-command.ts +6 -9
  253. package/scripts/rules/direct-filing.ts +3 -7
  254. package/scripts/rules/early-heartbeat.ts +7 -18
  255. package/scripts/rules/file-body.ts +6 -13
  256. package/scripts/rules/filing-cap.ts +4 -6
  257. package/scripts/rules/firing-point.ts +6 -5
  258. package/scripts/rules/git-force.ts +5 -9
  259. package/scripts/rules/guard-remedy.ts +117 -0
  260. package/scripts/rules/implementation-cut.ts +37 -35
  261. package/scripts/rules/index-guard.ts +4 -7
  262. package/scripts/rules/issue-comments.ts +67 -16
  263. package/scripts/rules/josh-git-bare.ts +3 -6
  264. package/scripts/rules/lane-background.ts +10 -21
  265. package/scripts/rules/lane-carry-conflict.ts +7 -17
  266. package/scripts/rules/lane-interactive-ask.ts +6 -11
  267. package/scripts/rules/lane-park.ts +5 -13
  268. package/scripts/rules/lane-split-park.ts +2 -7
  269. package/scripts/rules/lane-switch-main.ts +9 -12
  270. package/scripts/rules/last-prompt.ts +50 -5
  271. package/scripts/rules/oracle-consulted.ts +4 -5
  272. package/scripts/rules/oracle-list-cli.ts +7 -0
  273. package/scripts/rules/piped-verification.ts +84 -32
  274. package/scripts/rules/poll-loop.ts +7 -17
  275. package/scripts/rules/pre-gate-cut.ts +7 -21
  276. package/scripts/rules/prior-comment-read.ts +7 -5
  277. package/scripts/rules/protected-files.ts +5 -9
  278. package/scripts/rules/raw-field-body.ts +3 -7
  279. package/scripts/rules/rule-body-guard.ts +4 -11
  280. package/scripts/rules/rule-list.ts +4 -1
  281. package/scripts/rules/rule-registry-rows.ts +10 -0
  282. package/scripts/rules/rule-registry.ts +7 -4
  283. package/scripts/rules/rule-rewrite.ts +114 -0
  284. package/scripts/rules/rule-value.ts +7 -1
  285. package/scripts/rules/run-tail-rule.ts +6 -22
  286. package/scripts/rules/shell-body-trigger.ts +4 -6
  287. package/scripts/rules/shell-segments.ts +22 -0
  288. package/scripts/rules/stop-decision.ts +77 -0
  289. package/scripts/rules/stop-rules.ts +164 -77
  290. package/scripts/rules/tail-commands.ts +15 -1
  291. package/scripts/rules/test-declared-commit.ts +4 -10
  292. package/scripts/rules/third-party-write.ts +5 -10
  293. package/scripts/rules/worktree-guard.ts +5 -12
  294. package/scripts/run/board/run-board-cli.ts +2 -0
  295. package/scripts/run/board/run-board-closed.ts +8 -2
  296. package/scripts/run/board/run-board-github.ts +19 -3
  297. package/scripts/run/board/run-board-header.ts +4 -1
  298. package/scripts/run/board/run-board-idle.ts +31 -0
  299. package/scripts/run/board/run-board-labels.ts +2 -0
  300. package/scripts/run/board/run-board-layout.ts +1 -1
  301. package/scripts/run/board/run-board-read.ts +25 -12
  302. package/scripts/run/board/run-board-reason.ts +17 -4
  303. package/scripts/run/board/run-board-render-legend.ts +1 -0
  304. package/scripts/run/board/run-board-render-slot.ts +1 -1
  305. package/scripts/run/board/run-board-render-waits.ts +41 -0
  306. package/scripts/run/board/run-board-render.ts +6 -1
  307. package/scripts/run/board/run-board-row-yield.ts +1 -0
  308. package/scripts/run/board/run-board-state.ts +9 -0
  309. package/scripts/run/board/run-board-status.ts +89 -23
  310. package/scripts/run/board/run-board-thanks.ts +7 -2
  311. package/scripts/run/board/run-board-tick.ts +37 -8
  312. package/scripts/run/carry/run-carry-args.ts +1 -3
  313. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  314. package/scripts/run/cut/run-cut-cli.ts +18 -22
  315. package/scripts/run/cut/run-cut-decline.ts +72 -0
  316. package/scripts/run/cut/run-cut-ready.ts +74 -0
  317. package/scripts/run/cut/run-cut-report.ts +29 -0
  318. package/scripts/run/cut/run-cut.ts +10 -1
  319. package/scripts/run/detached-launch.ts +23 -4
  320. package/scripts/run/entry/run-entry-cli.ts +8 -1
  321. package/scripts/run/event/run-event-render.ts +1 -0
  322. package/scripts/run/event/run-event-scope.ts +19 -1
  323. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  324. package/scripts/run/event/run-event-stream.ts +4 -0
  325. package/scripts/run/hold/run-hold-cli.ts +14 -3
  326. package/scripts/run/hold/run-hold-session.ts +46 -0
  327. package/scripts/run/hold/run-hold.ts +13 -4
  328. package/scripts/run/launch-priority.ts +32 -0
  329. package/scripts/run/merge/run-merge-cli.ts +16 -42
  330. package/scripts/run/merge/run-merge-ship.ts +59 -1
  331. package/scripts/run/merge/run-merge-signals.ts +99 -0
  332. package/scripts/run/merge/run-merge-steps.ts +11 -1
  333. package/scripts/run/merge/run-merge.ts +22 -9
  334. package/scripts/run/run-ending.ts +31 -3
  335. package/scripts/run/run-liveness.ts +1 -1
  336. package/scripts/run/run-parent-cut-hook.ts +5 -10
  337. package/scripts/run/run-poll-hook.ts +157 -0
  338. package/scripts/run/run-preflight.ts +1 -2
  339. package/scripts/run/run-step-cli.ts +22 -15
  340. package/scripts/run/run-step-section.ts +85 -0
  341. package/scripts/run/run-stop-notify.ts +1 -2
  342. package/scripts/run/run-watcher-guard.ts +6 -3
  343. package/scripts/run/ship/run-ship-cli.ts +5 -12
  344. package/scripts/run/ship/run-ship-detach.ts +9 -0
  345. package/scripts/run/ship/run-ship-next.ts +10 -2
  346. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  347. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  348. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  349. package/scripts/run/ship/run-ship-return.ts +31 -7
  350. package/scripts/run/ship/run-ship-review-steps.ts +25 -14
  351. package/scripts/run/ship/run-ship-review.ts +15 -2
  352. package/scripts/run/ship/run-ship-steps.ts +1 -1
  353. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  354. package/scripts/run/ship/run-ship-sync.ts +14 -6
  355. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  356. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  357. package/scripts/safe-chain/project-config.ts +7 -2
  358. package/scripts/sync/sync-hook-safety.ts +8 -2
  359. package/scripts/test/test-e2e-guard.ts +30 -5
  360. package/scripts/test/test-unit-guard.ts +16 -1
  361. package/scripts/test/unit-worker-share.ts +26 -3
  362. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  363. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  364. package/scripts/time-runtime/time-shell.ts +39 -4
  365. package/scripts/time-runtime/time-spans.ts +1 -0
  366. package/scripts/time-runtime/time-tool-call.ts +7 -0
  367. package/scripts/version/effective-upstream.ts +3 -4
  368. package/scripts/version/kit-version-config.ts +8 -2
  369. package/scripts/version/latest-corepack.ts +1 -2
  370. package/scripts/version/release-hold.ts +11 -4
  371. package/scripts/version/version-commands.ts +12 -4
  372. package/templates/gitignore +6 -2
  373. package/dist/hooks/chunk-D5QBTOCZ.js +0 -68
  374. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -0,0 +1,182 @@
1
+ import { git_gh_issue_write } from '#scripts/gh/git-gh-issue-write'
2
+ import { issue_cite } from '#scripts/issue/issue-cite'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { issue_state_cli } from '#scripts/issue/issue-state-cli'
5
+ import { session_cite } from '#scripts/issue/session-cite'
6
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
7
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
8
+ import { run_label } from '#scripts/run/run-label'
9
+ import { lane_close, type CloseKind } from './lane-close'
10
+ import { lane_preserve, type KeptWork } from './lane-preserve'
11
+ import { lane_reap } from './lane-reap'
12
+ import { lane_registry, type LaneInfo } from './lane-registry'
13
+
14
+ // `josh lane:withdraw <N> --reason <text>` — a person takes a running lane child's issue out of the run
15
+ // for good: the child is stopped, its work stashed, the Issue closed as not planned and the lane closed.
16
+ //
17
+ // **The `withdraw` event is written first, before anything is stopped.** Killing the child alone would
18
+ // let `lane:await` book it stopped and `run:merge` classify it from GitHub — an open issue resumed as an
19
+ // `outage` or parked `needs-decision`. `run:merge` reads the event stream before GitHub, so a child whose
20
+ // newest launch-or-withdraw event is a withdraw is booked `withdrawn` whatever the Issue's state reads
21
+ // at that moment, and the race between the kill and the close cannot book it as anything else.
22
+ //
23
+ // **The issue is read again once the child is stopped.** It may merge between the first read and the
24
+ // reap; the reap stopped it, so the second read is final, and an issue it already closed keeps
25
+ // GitHub's close — `run:merge` books a merged close as merged whatever the event stream says.
26
+ //
27
+ // A tree that could not be stashed is left in place with the Issue open: closing the lane would delete
28
+ // the work, and the person decides what to keep.
29
+
30
+ const WITHDRAW_OCCASION = 'lane withdraw'
31
+ const WITHDRAW_CAUSE = 'a person withdrew it with `lane:withdraw`'
32
+ const CLOSED_STATE = 'CLOSED'
33
+ const CLOSED_KIND = 'closed'
34
+ const NO_LANE_KIND = 'no-lane'
35
+ const UNREADABLE_KIND = 'unreadable'
36
+
37
+ type Unsaved = Extract<KeptWork, { kind: 'unsaved' }>
38
+
39
+ interface Withdrawn {
40
+ kind: 'withdrawn'
41
+ // How many of the child's processes the withdrawal stopped — 0 for a child that had already ended.
42
+ reaped: number
43
+ stash: string | undefined
44
+ is_closed: boolean
45
+ // The issue had closed on its own — merged — before the withdrawal reached it, so it was left as is.
46
+ had_closed: boolean
47
+ lane: CloseKind
48
+ }
49
+
50
+ type IssueSettled = Pick<Withdrawn, 'had_closed' | 'is_closed'>
51
+
52
+ type Refused =
53
+ { kind: typeof CLOSED_KIND } | { kind: typeof NO_LANE_KIND } | { kind: typeof UNREADABLE_KIND }
54
+ type WithdrawOutcome = Withdrawn | Unsaved | Refused
55
+
56
+ function close_comment(reason: string): string {
57
+ return [
58
+ 'Withdrawn by a person with `pnpm josh lane:withdraw`.',
59
+ 'Its lane child was stopped and the lane closed.',
60
+ `- Why: ${reason}`,
61
+ ].join('\n')
62
+ }
63
+
64
+ async function record_withdraw(issue: string, reason: string): Promise<void> {
65
+ await run_event_stream_emit.emit(
66
+ run_event_stream.EVENT_KIND.WITHDRAW,
67
+ `${issue_cite.plain(issue)} withdrawn by a person: ${reason}`,
68
+ )
69
+ }
70
+
71
+ // A closed issue has nothing to withdraw, and an unread one might be: neither writes anything.
72
+ async function refusal_of(issue: string): Promise<Refused | undefined> {
73
+ const read = await issue_state_cli.read_issue(issue)
74
+
75
+ if (read.kind !== 'state') return { kind: UNREADABLE_KIND }
76
+ if (read.state.state.toUpperCase() === CLOSED_STATE) return { kind: CLOSED_KIND }
77
+
78
+ return undefined
79
+ }
80
+
81
+ async function close_open_issue(issue: string, reason: string): Promise<IssueSettled> {
82
+ const refusal = await refusal_of(issue)
83
+
84
+ // An unread issue may have merged during the reap, so it is left open for a person to close.
85
+ if (refusal !== undefined) {
86
+ const is_closed = refusal.kind === CLOSED_KIND
87
+
88
+ return { is_closed, had_closed: is_closed }
89
+ }
90
+
91
+ const is_closed = await git_gh_issue_write.issue_close_not_planned(issue, close_comment(reason))
92
+
93
+ return { is_closed, had_closed: false }
94
+ }
95
+
96
+ async function close_issue(
97
+ issue: string,
98
+ reason: string,
99
+ stash: string | undefined,
100
+ ): Promise<IssueSettled> {
101
+ const settled = await close_open_issue(issue, reason)
102
+
103
+ if (stash !== undefined) await lane_preserve.announce(issue, stash, WITHDRAW_CAUSE)
104
+ await run_label.unmark(issue)
105
+
106
+ return settled
107
+ }
108
+
109
+ async function stop_and_close(lane: LaneInfo, reason: string): Promise<WithdrawOutcome> {
110
+ await record_withdraw(lane.issue, reason)
111
+ const reaped = lane_reap.reap_child(lane.issue).length
112
+ const saved = await lane_preserve.keep_work(lane.issue, lane.directory, WITHDRAW_OCCASION)
113
+
114
+ if (saved.kind === 'unsaved') return saved
115
+ const settled = await close_issue(lane.issue, reason, saved.stash)
116
+ const closed = await lane_close.close_lane(lane.issue)
117
+
118
+ return { kind: 'withdrawn', reaped, stash: saved.stash, ...settled, lane: closed.kind }
119
+ }
120
+
121
+ /** Stop `#<N>`'s lane child, stash its work, close the Issue as not planned and close the lane. */
122
+ async function withdraw(issue: string, reason: string): Promise<WithdrawOutcome> {
123
+ issue_number_shape.require_issue_number(issue)
124
+ const refusal = await refusal_of(issue)
125
+
126
+ if (refusal !== undefined) return refusal
127
+ const lane = await lane_registry.find_open_lane(issue)
128
+
129
+ if (lane === undefined) return { kind: NO_LANE_KIND }
130
+
131
+ return await stop_and_close(lane, reason)
132
+ }
133
+
134
+ function stash_sentence(stash: string | undefined): string {
135
+ if (stash === undefined) return 'The lane held no uncommitted work.'
136
+
137
+ return `Its uncommitted work was stashed — recover with \`pnpm josh stash:pop "${stash}"\`.`
138
+ }
139
+
140
+ function withdrawn_text(outcome: Withdrawn, issue: string): string {
141
+ const cited = session_cite.issue(issue)
142
+ const stopped = outcome.reaped === 0 ? 'Its child had already ended.' : 'Stopped its child.'
143
+ const head = `Withdrew ${cited}. ${stopped} ${stash_sentence(outcome.stash)}`
144
+ const lane = outcome.lane === 'incomplete' ? ' The lane could not be fully removed.' : ''
145
+
146
+ if (outcome.had_closed) return `${head} The Issue had already closed and was left as is.${lane}`
147
+
148
+ if (outcome.is_closed) return `${head} The Issue is closed as not planned.${lane}`
149
+
150
+ return `${head} The Issue could not be closed — close ${cited} by hand; \`run:merge\` books it withdrawn either way.${lane}`
151
+ }
152
+
153
+ function unsaved_text(outcome: Unsaved, issue: string): string {
154
+ return `The child for ${session_cite.issue(issue)} was stopped, but its uncommitted work could not be stashed (${outcome.reason}), so the Issue is left open and the lane at ${outcome.directory}; \`run:merge\` books it withdrawn.`
155
+ }
156
+
157
+ function refused_text(outcome: Refused, issue: string): string {
158
+ const cited = session_cite.issue(issue)
159
+
160
+ if (outcome.kind === CLOSED_KIND) {
161
+ return `${cited} is already closed, so there is nothing to withdraw.`
162
+ }
163
+
164
+ if (outcome.kind === NO_LANE_KIND) {
165
+ return `No lane is open for ${cited}, so there is no child to withdraw.`
166
+ }
167
+
168
+ return `The state of ${cited} could not be read, so nothing was stopped or closed — try again.`
169
+ }
170
+
171
+ function describe(outcome: WithdrawOutcome, issue: string): string {
172
+ if (outcome.kind === 'unsaved') return unsaved_text(outcome, issue)
173
+
174
+ return outcome.kind === 'withdrawn'
175
+ ? withdrawn_text(outcome, issue)
176
+ : refused_text(outcome, issue)
177
+ }
178
+
179
+ const lane_withdraw = { describe, withdraw }
180
+
181
+ export type { WithdrawOutcome }
182
+ export { lane_withdraw }
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { project_checks } from '#scripts/gate/project-checks'
4
- import { ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
4
+ import { ESLINT_CACHE_FLAGS, PRETTIER_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
5
5
  import { composite_arguments, USAGE_ERROR_EXIT_CODE } from '#scripts/josh/josh-composite-arguments'
6
6
  import { execa } from 'execa'
7
7
 
8
8
  const COMMAND_NAME = 'format'
9
9
  const ARGV_OFFSET = 2
10
10
  const FAIL_EXIT_CODE = 1
11
- const PRETTIER_ARGS = ['exec', 'prettier', '--write', '.'] as const
11
+ const PRETTIER_WRITE_ARGS = ['exec', 'prettier', '--write', '.', ...PRETTIER_CACHE_FLAGS] as const
12
12
  const ESLINT_FIX_ARGS = ['exec', 'eslint', '.', '--fix', ...ESLINT_CACHE_FLAGS] as const
13
13
 
14
14
  async function run_step(args: ReadonlyArray<string>, directory: string): Promise<number> {
@@ -37,7 +37,7 @@ async function run_format(directory: string): Promise<number> {
37
37
  const prettier_exit_code = await run_unless_skipped(
38
38
  'prettier',
39
39
  prettier_reason,
40
- PRETTIER_ARGS,
40
+ PRETTIER_WRITE_ARGS,
41
41
  directory,
42
42
  )
43
43
  if (prettier_exit_code !== 0) return prettier_exit_code
@@ -64,4 +64,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
64
64
 
65
65
  const format = { run, run_format }
66
66
 
67
- export { ESLINT_FIX_ARGS, format }
67
+ export { ESLINT_FIX_ARGS, format, PRETTIER_WRITE_ARGS }
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { project_checks } from '#scripts/gate/project-checks'
4
- import { ESLINT_CACHE_FILE, ESLINT_CACHE_FLAGS } from '#scripts/josh/josh-command-types'
4
+ import {
5
+ ESLINT_CACHE_FILE,
6
+ ESLINT_CACHE_FLAGS,
7
+ PRETTIER_CACHE_FLAGS,
8
+ } from '#scripts/josh/josh-command-types'
5
9
  import { lane_cache_run } from '#scripts/lane/lane-cache-run'
6
10
  import {
7
11
  buffered_process,
@@ -11,7 +15,6 @@ import {
11
15
  import { SKIP_MARKER } from '#scripts/test/skip-marker'
12
16
  import { cross_file_recheck, type CrossFileRecheckOptions } from './cross-file-recheck'
13
17
 
14
- const PRETTIER_ARGS = ['exec', 'prettier', '--check', '.'] as const
15
18
  // This — not the `lint:eslint` map entry — is the eslint invocation `josh gate` reaches, so the
16
19
  // cache flags come from the same constant the ignore rules are asserted against.
17
20
  // A copy here would keep writing the old location the day that constant
@@ -23,6 +26,10 @@ const WHOLE_TREE_RECHECK: CrossFileRecheckOptions = {
23
26
  patterns: [WHOLE_TREE],
24
27
  }
25
28
 
29
+ // Prettier's cache stays this checkout's own, never shared through `lane_cache_run` as eslint's is:
30
+ // its entries do not survive a move (`josh-command-types.ts` → `GATE_CACHE_SPECS`).
31
+ const PRETTIER_ARGS = ['exec', 'prettier', '--check', WHOLE_TREE, ...PRETTIER_CACHE_FLAGS] as const
32
+
26
33
  // What one eslint run is made of: the child's arguments, and what re-reading a cached cross-file report
27
34
  // (`cross-file-recheck.ts`) needs to ask the same question again — the cache file and the targets.
28
35
  interface EslintRun {
@@ -132,4 +139,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
132
139
 
133
140
  const lint_parallel = { run_lint_checks, run_lint_parallel_checks }
134
141
 
135
- export { ESLINT_ARGS, lint_parallel }
142
+ export { ESLINT_ARGS, lint_parallel, PRETTIER_ARGS }
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  ESLINT_RELATED_CACHE_FILE,
10
10
  ESLINT_RELATED_CACHE_FLAGS,
11
+ PRETTIER_RELATED_CACHE_FLAGS,
11
12
  } from '#scripts/josh/josh-command-types'
12
13
  import type { CrossFileRecheckOptions } from './cross-file-recheck'
13
14
 
@@ -94,8 +95,10 @@ function describe_scope(scope: ChangedFileScope, root: string): string {
94
95
  return changed_file_scope.describe_scope(scope, root, VOCABULARY)
95
96
  }
96
97
 
98
+ // The scoped run's own cache file, for the reason eslint's below gives: prettier too rewrites its
99
+ // cache file whole, so a narrowed run sharing the gate's would discard entries a concurrent gate wrote.
97
100
  function prettier_arguments(files: ReadonlyArray<string>): ReadonlyArray<string> {
98
- return [...PRETTIER_ARGS, ...files]
101
+ return [...PRETTIER_ARGS, ...files, ...PRETTIER_RELATED_CACHE_FLAGS]
99
102
  }
100
103
 
101
104
  // The cache file is this command's own rather than the gate's, because the two run at the same time.
@@ -46,8 +46,7 @@ function apply_marker_for_destination(
46
46
  text: string,
47
47
  package_name: string,
48
48
  ): string {
49
- if (!is_workflow_destination(destination)) return text
50
- if (is_marked(text)) return text
49
+ if (!is_workflow_destination(destination) || is_marked(text)) return text
51
50
 
52
51
  return `${marker_block(package_name)}${text}`
53
52
  }
@@ -112,6 +112,14 @@ async function resolve_context(values: CliValues): Promise<ResolvedContext> {
112
112
  return { repo_name, issue_title }
113
113
  }
114
114
 
115
+ // The send behind the command line, for a caller that already holds the values — the `Stop` hook's
116
+ // own confirmation (`stop-actions.ts`). It throws as `telegram_notify.send` does.
117
+ async function send_values(values: CliValues): Promise<void> {
118
+ const context = await resolve_context(values)
119
+
120
+ await telegram_notify.send(telegram_test_logic.build_input({ values, context }))
121
+ }
122
+
115
123
  async function main(): Promise<void> {
116
124
  josh_environment_file.load_environment_file()
117
125
  const values = parse_cli_arguments()
@@ -122,10 +130,7 @@ async function main(): Promise<void> {
122
130
  return
123
131
  }
124
132
 
125
- const context = await resolve_context(values)
126
- const input = telegram_test_logic.build_input({ values, context })
127
-
128
- await telegram_notify.send(input)
133
+ await send_values(values)
129
134
  }
130
135
 
131
136
  // **A notification nobody received must not read as success**. This command
@@ -148,6 +153,6 @@ async function run_cli(): Promise<void> {
148
153
 
149
154
  if (process.argv[1] === fileURLToPath(import.meta.url)) await run_cli()
150
155
 
151
- const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli }
156
+ const telegram_test = { fetch_repo_name, fetch_issue_title, resolve_context, run_cli, send_values }
152
157
 
153
158
  export { telegram_test }
@@ -42,9 +42,7 @@ function find_entry_mismatch(
42
42
  specifier: string,
43
43
  override: string | undefined,
44
44
  ): Array<SpecifierMismatch> {
45
- if (override === undefined) return []
46
- if (is_non_registry(specifier)) return []
47
- if (specifier === override) return []
45
+ if (override === undefined || specifier === override || is_non_registry(specifier)) return []
48
46
 
49
47
  return [{ importer, name, lockfile_specifier: specifier, override }]
50
48
  }
@@ -277,8 +277,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
277
277
  }
278
278
 
279
279
  const plan = plan_run(options)
280
- if (plan === undefined) return FAILURE_EXIT_CODE
281
- if (!(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
280
+ if (plan === undefined || !(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
282
281
 
283
282
  return propagate_to_consumers(plan, options.is_dry_run)
284
283
  }
@@ -145,8 +145,9 @@ async function wait_for_github_release(version: string): Promise<boolean> {
145
145
  async function wait_for_distribution(version: string): Promise<number> {
146
146
  const manifest = read_manifest(process.cwd())
147
147
 
148
- if (!(await wait_for_npm(manifest, version))) return FAILURE_EXIT_CODE
149
- if (!(await wait_for_github_release(version))) return FAILURE_EXIT_CODE
148
+ if (!(await wait_for_npm(manifest, version)) || !(await wait_for_github_release(version))) {
149
+ return FAILURE_EXIT_CODE
150
+ }
150
151
 
151
152
  console.info(complete_line(version))
152
153
 
@@ -8,6 +8,7 @@ const REPORT_FORMAT_PATH = 'prompts/collaboration-workflow/report-format.md'
8
8
  const SUMMARY_RULES_HEADING = '作業前サマリはいつ・どう出すか'
9
9
  const SUMMARY_TEMPLATE_HEADING = '作業前サマリ(セッション向け・`JOSH_SESSION_LANG` の言語)'
10
10
  const COMPLETION_REPORT_HEADING = '完了報告(セッション向け)'
11
+ const ACCEPTANCE_MAP_HEADING = 'PR 本文の受け入れ条件の対応表'
11
12
 
12
13
  // The heading goes in single quotes in the runnable form: the template heading carries backticks,
13
14
  // which a double-quoted shell argument would run as a command substitution.
@@ -16,6 +17,7 @@ function pointer(heading: string): string {
16
17
  }
17
18
 
18
19
  const report_format_reference = {
20
+ ACCEPTANCE_MAP_HEADING,
19
21
  COMPLETION_REPORT_HEADING,
20
22
  pointer,
21
23
  REPORT_FORMAT_PATH,
@@ -102,8 +102,7 @@ function overview_lines(summary: string): ReadonlyArray<string> {
102
102
  function item_sentence(line: string): string {
103
103
  const open = line.indexOf(BOLD_MARKER)
104
104
  const close = open === -1 ? -1 : line.indexOf(BOLD_MARKER, open + BOLD_MARKER.length)
105
- const raw =
106
- close === -1 ? line.slice(ITEM_LINE_PREFIX.length) : line.slice(close + BOLD_MARKER.length)
105
+ const raw = line.slice(close === -1 ? ITEM_LINE_PREFIX.length : close + BOLD_MARKER.length)
107
106
 
108
107
  return raw.replace(LEADING_LABEL_PUNCTUATION, '').trim()
109
108
  }
@@ -199,8 +198,12 @@ function break_condition_count(lines: ReadonlyArray<string>): number {
199
198
  }
200
199
 
201
200
  function case_line_violations(summary: string): ReadonlyArray<string> {
202
- if (!has_changes_section(summary)) return []
203
- if (changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))) return []
201
+ if (
202
+ !has_changes_section(summary) ||
203
+ changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))
204
+ ) {
205
+ return []
206
+ }
204
207
 
205
208
  return [`✖ no case tier (${CASE_LABEL}:) in ${CHANGES_LABEL}`]
206
209
  }
@@ -0,0 +1,61 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { session_cite } from '#scripts/issue/session-cite'
3
+ import { acceptance_map, type Deferral } from './acceptance-map'
4
+
5
+ // The reads behind `acceptance_map`: the Issue as it reads at the merge, the pull request body, and
6
+ // whether each issue a deferral row names exists. Kept apart so the comparison stays pure.
7
+
8
+ const ISSUE_SEPARATOR = '#'
9
+
10
+ // `owner/repo#12` reads that repository's issue; a bare `#12` reads this one's.
11
+ async function is_found(deferral: Deferral): Promise<boolean> {
12
+ const [repo, number = ''] = deferral.issue.split(ISSUE_SEPARATOR)
13
+ const read = await git_gh_command.issue_get_body_classified(
14
+ number,
15
+ repo === '' ? undefined : repo,
16
+ )
17
+
18
+ return read.kind === 'read'
19
+ }
20
+
21
+ async function absent_deferrals(
22
+ deferrals: ReadonlyArray<Deferral>,
23
+ ): Promise<ReadonlyArray<string>> {
24
+ const found = await Promise.all(deferrals.map(async (deferral) => await is_found(deferral)))
25
+
26
+ return deferrals
27
+ .filter((_deferral, index) => found[index] !== true)
28
+ .map((deferral) => `- \`${deferral.id}\` names ${deferral.issue}`)
29
+ }
30
+
31
+ function unreadable_message(issue: string): string {
32
+ return `Issue ${session_cite.issue(issue)} could not be re-read to check its acceptance criteria, so the merge is refused; re-run once it can be.`
33
+ }
34
+
35
+ // **An Issue that cannot be re-read is a refusal, not a pass** — the criteria it would have named are
36
+ // exactly what this gate exists to see. An Issue with no acceptance section merges as before.
37
+ // `josh ship`'s preflight asks the body it is about to deliver, so a missing table stops it before the
38
+ // review rather than at the merge.
39
+ async function body_refusal(
40
+ issue: string,
41
+ pr_body: string | undefined,
42
+ ): Promise<string | undefined> {
43
+ const issue_read = await git_gh_command.issue_get_body_classified(issue)
44
+
45
+ if (issue_read.kind !== 'read') return unreadable_message(issue)
46
+ const criteria = acceptance_map.criteria_of(issue_read.text)
47
+ const comparison = acceptance_map.compare(criteria, acceptance_map.rows_of(pr_body))
48
+ const absent = await absent_deferrals(comparison.deferrals)
49
+
50
+ if (comparison.missing.length === 0 && absent.length === 0) return undefined
51
+
52
+ return acceptance_map.refusal_message(issue, comparison, absent)
53
+ }
54
+
55
+ async function refusal(issue: string, branch_name: string): Promise<string | undefined> {
56
+ return await body_refusal(issue, await git_gh_command.pr_get_body(branch_name))
57
+ }
58
+
59
+ const acceptance_map_check = { body_refusal, refusal }
60
+
61
+ export { acceptance_map_check }
@@ -0,0 +1,205 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { epic_audit_logic } from '#scripts/epic/epic-audit'
3
+ import { markdown_section } from '#scripts/issue/markdown-section'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { report_format_reference } from '#scripts/report/report-format-reference'
6
+
7
+ // A green gate and a clean review say the diff is sound, never that it delivers what the Issue asked
8
+ // for. **This is the seam where each acceptance criterion is checked one by one**: the pull request
9
+ // body carries one row per criterion — met, with its evidence, or deferred to a split-off issue — and
10
+ // `josh followup` re-reads the Issue just before the merge and refuses a criterion with no row.
11
+ //
12
+ // **A criterion is matched by an identifier built from its own text**, so a criterion added or
13
+ // reworded after the body was written has an identifier no row carries, and is refused as unmapped
14
+ // rather than passing on a stale row. The checkbox is stripped first: ticking it changes nothing.
15
+ //
16
+ // The criteria are read through `epic_audit_logic.acceptance_section`, the one place the acceptance
17
+ // headings are defined.
18
+
19
+ const MAP_HEADING = '## 受け入れ条件の対応'
20
+ const MET = '満たした'
21
+ const DEFERRED = '見送り'
22
+ const PENDING = '未記入'
23
+ const PENDING_SUFFIX = `— ${PENDING}`
24
+ const ID_PREFIX = 'ac-'
25
+ const ID_HEX_LENGTH = 8
26
+ const BACKTICK = '`'
27
+ // A top-level list item only: a nested item is detail under a criterion, not one of its own.
28
+ const CRITERION_PATTERN = /^(?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s+)?(?<text>\S.*)$/u
29
+ // A list item, heading, table row or HTML comment opens a block of its own, never a wrapped line.
30
+ const BLOCK_START_PATTERN = /^\s*(?:(?:[-*+]|\d+\.)\s|#|\||<!--)/u
31
+ // The criterion's text may sit between the identifier and the status for the reader, so the status is
32
+ // the last status word on the line — a criterion quoting the row format stays text.
33
+ const ROW_PATTERN = new RegExp(
34
+ String.raw`^[-*]\s+${BACKTICK}(?<id>${ID_PREFIX}[0-9a-f]{${String(ID_HEX_LENGTH)}})${BACKTICK}(?:\s.*)?\s(?<kind>${MET}|${DEFERRED}):\s*(?<rest>\S.*)$`,
35
+ 'u',
36
+ )
37
+ // A deferral names the split-off issue first, then why it was deferred.
38
+ const DEFERRAL_PATTERN = /^(?<issue>(?:[\w.-]+\/[\w.-]+)?#\d+)\s+\S/u
39
+ const ROW_FORMAT = `under ${MAP_HEADING}, one line per acceptance criterion: \`- \`<id>\` <criterion> — ${MET}: <test name, file:line or command output>\` or \`- \`<id>\` <criterion> — ${DEFERRED}: #<split-off issue> <reason>\``
40
+
41
+ interface Criterion {
42
+ id: string
43
+ text: string
44
+ }
45
+
46
+ interface Deferral {
47
+ id: string
48
+ issue: string
49
+ }
50
+
51
+ interface Row {
52
+ id: string
53
+ kind: string
54
+ rest: string
55
+ }
56
+
57
+ interface MapRows {
58
+ met: ReadonlySet<string>
59
+ deferrals: ReadonlyArray<Deferral>
60
+ ids: ReadonlySet<string>
61
+ }
62
+
63
+ interface MapComparison {
64
+ missing: ReadonlyArray<Criterion>
65
+ deferrals: ReadonlyArray<Deferral>
66
+ stale: ReadonlyArray<string>
67
+ }
68
+
69
+ function criterion_id(text: string): string {
70
+ const normalized = text.replaceAll(/\s+/gu, ' ').trim()
71
+ const digest = createHash('sha256').update(normalized).digest('hex')
72
+
73
+ return `${ID_PREFIX}${digest.slice(0, ID_HEX_LENGTH)}`
74
+ }
75
+
76
+ function joins(last: string | undefined, line: string): boolean {
77
+ const is_continuation = line.trim() !== '' && !BLOCK_START_PATTERN.test(line)
78
+
79
+ return is_continuation && last !== undefined && CRITERION_PATTERN.test(last)
80
+ }
81
+
82
+ // A criterion wrapped onto continuation lines is one criterion, hashed whole — rewording any of its
83
+ // lines changes its identifier. A blank line or a nested item ends it.
84
+ function unwrapped(lines: ReadonlyArray<string>): ReadonlyArray<string> {
85
+ const items: Array<string> = []
86
+
87
+ for (const line of lines) {
88
+ items.push(joins(items.at(-1), line) ? `${items.pop() ?? ''} ${line.trim()}` : line)
89
+ }
90
+
91
+ return items
92
+ }
93
+
94
+ function criteria_of(issue_body: string | undefined): ReadonlyArray<Criterion> {
95
+ // A list item inside a code fence is an example the Issue quotes, not a criterion. An indented line
96
+ // outside one is a wrapped criterion's continuation, so it is kept.
97
+ const lines = markdown_section.fence_free_lines(epic_audit_logic.acceptance_section(issue_body))
98
+ const texts = unwrapped(lines).map((line) => CRITERION_PATTERN.exec(line)?.groups?.['text'])
99
+
100
+ return texts
101
+ .filter((text): text is string => text !== undefined)
102
+ .map((text) => ({ id: criterion_id(text), text: text.trim() }))
103
+ }
104
+
105
+ // A deferral row with no issue or no reason is no row at all, so its criterion reads as unmapped.
106
+ function deferral_of(id: string, rest: string): Deferral | undefined {
107
+ const issue = DEFERRAL_PATTERN.exec(rest)?.groups?.['issue']
108
+
109
+ return issue === undefined ? undefined : { id, issue }
110
+ }
111
+
112
+ // Every group in `ROW_PATTERN` is mandatory, so a match always carries the name.
113
+ function group(groups: Readonly<Record<string, string>>, name: string): string {
114
+ return groups[name] ?? ''
115
+ }
116
+
117
+ // A pending row is no row even when the criterion it quotes carries a status word — otherwise the
118
+ // skeleton's own unfilled row would read as met.
119
+ function row_of(line: string): Row | undefined {
120
+ const trimmed = line.trim()
121
+
122
+ if (trimmed.endsWith(PENDING_SUFFIX)) return undefined
123
+ const groups = ROW_PATTERN.exec(trimmed)?.groups
124
+
125
+ if (groups === undefined) return undefined
126
+
127
+ return { id: group(groups, 'id'), kind: group(groups, 'kind'), rest: group(groups, 'rest') }
128
+ }
129
+
130
+ function rows_of(pr_body = ''): MapRows {
131
+ const rows = markdown_section
132
+ .section_lines(pr_body, MAP_HEADING)
133
+ .map((line) => row_of(line))
134
+ .filter((row): row is Row => row !== undefined)
135
+ const met = rows.filter((row) => row.kind === MET).map((row) => row.id)
136
+ const deferrals = rows
137
+ .filter((row) => row.kind === DEFERRED)
138
+ .map((row) => deferral_of(row.id, row.rest))
139
+ .filter((deferral): deferral is Deferral => deferral !== undefined)
140
+
141
+ return { met: new Set(met), deferrals, ids: new Set([...met, ...deferrals.map((row) => row.id)]) }
142
+ }
143
+
144
+ function compare(criteria: ReadonlyArray<Criterion>, rows: MapRows): MapComparison {
145
+ const current = new Set(criteria.map((criterion) => criterion.id))
146
+
147
+ return {
148
+ missing: criteria.filter((criterion) => !rows.ids.has(criterion.id)),
149
+ deferrals: rows.deferrals.filter((deferral) => current.has(deferral.id)),
150
+ stale: [...rows.ids].filter((id) => !current.has(id)),
151
+ }
152
+ }
153
+
154
+ // What `josh pr` writes when the caller's body carries no table: every criterion as a pending row,
155
+ // which the merge refuses until it is filled in.
156
+ function skeleton(issue_body: string | undefined): string | undefined {
157
+ const criteria = criteria_of(issue_body)
158
+
159
+ if (criteria.length === 0) return undefined
160
+ const rows = criteria.map(
161
+ (criterion) => `- \`${criterion.id}\` ${criterion.text} ${PENDING_SUFFIX}`,
162
+ )
163
+
164
+ return [MAP_HEADING, '', ...rows].join('\n')
165
+ }
166
+
167
+ function has_map(pr_body = ''): boolean {
168
+ return markdown_section.has_unfenced_line(pr_body, MAP_HEADING)
169
+ }
170
+
171
+ function listed(title: string, lines: ReadonlyArray<string>): ReadonlyArray<string> {
172
+ return lines.length === 0 ? [] : [title, ...lines.map((line) => ` ${line}`)]
173
+ }
174
+
175
+ function refusal_message(
176
+ issue: string,
177
+ comparison: MapComparison,
178
+ absent: ReadonlyArray<string>,
179
+ ): string {
180
+ return [
181
+ `The pull request body does not map every acceptance criterion of ${session_cite.issue(issue)} as it reads now, so the merge is refused.`,
182
+ ...listed(
183
+ 'Criteria with no row (added or reworded since the body was written, or never mapped):',
184
+ comparison.missing.map((criterion) => `- \`${criterion.id}\` ${criterion.text}`),
185
+ ),
186
+ ...listed('Deferral rows naming an issue that could not be found:', absent),
187
+ ...listed('Rows naming no current criterion:', comparison.stale),
188
+ `Write ${ROW_FORMAT} — format: ${report_format_reference.pointer(report_format_reference.ACCEPTANCE_MAP_HEADING)}`,
189
+ 'Write it into the `--body-file` before `josh ship`, or update an open PR with `gh api -X PATCH repos/{owner}/{repo}/pulls/<N> -F body=@<path>`, then re-run.',
190
+ ].join('\n')
191
+ }
192
+
193
+ const acceptance_map = {
194
+ MAP_HEADING,
195
+ compare,
196
+ criteria_of,
197
+ criterion_id,
198
+ has_map,
199
+ refusal_message,
200
+ rows_of,
201
+ skeleton,
202
+ }
203
+
204
+ export type { Criterion, Deferral, MapComparison }
205
+ export { acceptance_map }
@@ -330,7 +330,7 @@ async function run_review(round: number, reading: ChangeReading): Promise<number
330
330
  const refusal = scoped_green.refusal_for(tree, commit)
331
331
 
332
332
  if (refusal !== undefined) return report_error(refusal)
333
- const resolved = agent_role_profile.resolve(agent_role_profile.REVIEWER)
333
+ const resolved = agent_role_profile.resolve_review(round)
334
334
 
335
335
  if (resolved.kind === 'rejected') return report_error(resolved.note)
336
336
  agent_role_profile.warn_of_default()