@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
@@ -5,6 +5,7 @@ import { agent_headless } from '#scripts/agent/agent-headless'
5
5
  import { backlog_ready } from '#scripts/backlog/backlog-ready'
6
6
  import { backlog_stalled_detect } from '#scripts/backlog/backlog-stalled-detect'
7
7
  import { repo_party } from '#scripts/discovery/repo-party'
8
+ import { stop_actions } from '#scripts/hooks/stop-actions'
8
9
  import { hook_decision } from '#scripts/josh/hook-decision'
9
10
  import { session_language } from '#scripts/josh/session-language'
10
11
  import { lane_child_marker } from '#scripts/lane/lane-child-marker'
@@ -13,37 +14,64 @@ import { filing_cap } from '#scripts/rules/filing-cap'
13
14
  import { lane_background } from '#scripts/rules/lane-background'
14
15
  import { lane_park } from '#scripts/rules/lane-park'
15
16
  import { last_prompt } from '#scripts/rules/last-prompt'
17
+ import { pre_gate_cut } from '#scripts/rules/pre-gate-cut'
18
+ import { stop_decision } from '#scripts/rules/stop-decision'
16
19
  import { stop_rules, type StopContext, type StopOutcome } from '#scripts/rules/stop-rules'
20
+ import { tail_commands } from '#scripts/rules/tail-commands'
17
21
  import { run_carry } from '#scripts/run/carry/run-carry'
18
22
  import { run_cut } from '#scripts/run/cut/run-cut'
19
- import { run_hold } from '#scripts/run/hold/run-hold'
23
+ import { run_cut_report } from '#scripts/run/cut/run-cut-report'
24
+ import { run_hold, type RunHold } from '#scripts/run/hold/run-hold'
25
+ import { run_hold_session } from '#scripts/run/hold/run-hold-session'
20
26
  import { run_halfrun_resume } from '#scripts/run/run-halfrun-resume'
21
27
  import { run_headless } from '#scripts/run/run-headless'
22
28
  import { run_stranded_detect } from '#scripts/run/run-stranded-detect'
23
29
  import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
24
30
  import { time_hook_transcript } from '#scripts/time-runtime/time-hook-transcript'
25
31
 
26
- // The `Stop` hook entry: one process that delivers the three stop-time rules.
32
+ // The `Stop` hook entry: one process that delivers the stop-time rules, running the fixed ones itself
33
+ // (`stop-actions.ts`).
27
34
  // It is the stop-time counterpart of `pretool-guard.ts` — a second entry on the same `hook_decision`
28
35
  // foundation, not a second copy of it.
29
36
  //
30
37
  // **Every failure allows the stop.** A missing transcript, an unreadable hold record, a git status
31
38
  // that will not read — each ends as "no block", for the same reason the PreToolUse guard fails open:
32
- // a hook that failed closed would wedge a run over its own plumbing. The block rules are self-
33
- // resolving (send the notify, release the hold) and `stop_hook_active` is the backstop, so failing
39
+ // a hook that failed closed would wedge a run over its own plumbing. The hook runs the fixed
40
+ // operations itself (send the notify, release the hold) and `stop_hook_active` is the backstop, so failing
34
41
  // open here costs at most a missed nudge, never a stuck run.
35
42
 
36
- // The hold record for this working tree is still in place, and whether it is a `prrun` stop's.
37
- // `unreadable` reads as absent: the safe direction for a stop guard is not to
38
- // block, and an unreadable record is not proof a run is holding.
39
- async function hold_facts(): Promise<Pick<StopContext, 'hold_present' | 'prrun_stopped'>> {
43
+ // The hold record for this working tree. `unreadable` reads as absent: the safe direction for a stop
44
+ // guard is not to act, and an unreadable record is not proof a run is holding.
45
+ async function held_record(): Promise<RunHold | undefined> {
40
46
  const directory = await run_hold.worktree_directory()
41
- const hold =
42
- directory === undefined
43
- ? undefined
44
- : run_halfrun_resume.hold_of(run_hold.read_hold(run_hold.hold_path(directory)))
47
+ if (directory === undefined) return undefined
45
48
 
46
- return { hold_present: hold !== undefined, prrun_stopped: hold?.prrun_stop_head !== undefined }
49
+ return run_halfrun_resume.hold_of(run_hold.read_hold(run_hold.hold_path(directory)))
50
+ }
51
+
52
+ // The pause was announced: by a `josh notify` on the tail, or by this hook's own send, recorded on the
53
+ // hold since the pause began (joshuafolkken/kit#3961).
54
+ function was_notified(hold: RunHold | undefined, tail: string): boolean {
55
+ if (lane_park.mentions_confirmation_notify(tail)) return true
56
+
57
+ return (
58
+ hold !== undefined && run_hold_session.is_notified_since(hold, last_prompt.pause_start(tail))
59
+ )
60
+ }
61
+
62
+ // The Issue the hold names, whether it is a `prrun` stop's and this session's own, and whether its
63
+ // pause was already announced.
64
+ async function hold_facts(
65
+ tail: string,
66
+ ): Promise<Pick<StopContext, 'hold_issue' | 'hold_owned' | 'notified' | 'prrun_stopped'>> {
67
+ const hold = await held_record()
68
+
69
+ return {
70
+ hold_issue: hold?.issue,
71
+ hold_owned: hold === undefined || run_hold_session.is_session_hold(hold),
72
+ notified: was_notified(hold, tail),
73
+ prrun_stopped: hold?.prrun_stop_head !== undefined,
74
+ }
47
75
  }
48
76
 
49
77
  // A `confirmation` notify sits on this run's transcript tail. The tail is derived exactly as the
@@ -60,6 +88,19 @@ function was_filed(tail: string): boolean {
60
88
  return filing_cap.turn_filing_count(tail) > 0
61
89
  }
62
90
 
91
+ // This turn ended at a cut — a record still `carried`, or this turn's `run:cut` relaunching a successor.
92
+ // The second covers the successor's resume clearing the record before this stop (joshuafolkken/kit#3936);
93
+ // the held cut outside a lane prints no marker, so its person is still notified.
94
+ function is_cut_stop(tail: string): boolean {
95
+ if (run_cut.carried_cut_sync() !== undefined) return true
96
+
97
+ return tail_commands.answered(
98
+ filing_cap.current_turn(tail),
99
+ pre_gate_cut.takes_the_cut,
100
+ run_cut_report.RELAUNCHED_CUT_MARKER,
101
+ )
102
+ }
103
+
63
104
  // The session language the refusal is written for — `write_stop_decision` has already loaded the `.env`.
64
105
  function session_lang(): string {
65
106
  return session_language.resolve_session_lang().lang
@@ -85,13 +126,13 @@ async function build_context(
85
126
  const tail = transcript_tail(transcript_path)
86
127
 
87
128
  return {
88
- ...(await hold_facts()),
129
+ ...(await hold_facts(tail)),
89
130
  tree_clean: !(await run_hold.is_tree_dirty()),
90
- notified: lane_park.mentions_confirmation_notify(tail),
91
131
  message: payload_tail.message,
92
132
  prompt: last_prompt.prompt_text(tail),
133
+ answers_question: stop_decision.is_question(last_prompt.turn_prompt(tail)),
93
134
  stop_hook_active: payload_tail.stop_hook_active,
94
- cut_pending: run_cut.carried_cut_sync() !== undefined,
135
+ cut_pending: is_cut_stop(tail),
95
136
  filed: was_filed(tail),
96
137
  session_owner: repo_party.current_owner(),
97
138
  ...(await session_role_facts()),
@@ -167,17 +208,17 @@ async function run_report_checks(): Promise<void> {
167
208
  await run_stranded_detect.run_stranded_check()
168
209
  }
169
210
 
170
- // Nothing reaches stdout on an ordinary stop, so what the harness parses stays empty unless the stop
171
- // is being held — all three rules block, so a bare `#N` is reported the same way.
211
+ // Nothing reaches stdout on an ordinary stop, so what the harness parses stays empty unless the stop is
212
+ // held or the hook acted on it (`stop-actions.ts`).
172
213
  async function write_stop_decision(raw_payload: string): Promise<void> {
173
214
  hook_decision.load_environment_file()
174
215
  await run_report_checks()
175
216
 
176
- const { reason } = await outcome_of(raw_payload)
217
+ const envelope = await stop_actions.envelope_of(await outcome_of(raw_payload), session_lang())
177
218
 
178
- if (reason === undefined) return
219
+ if (envelope === undefined) return
179
220
 
180
- process.stdout.write(`${stop_rules.block_envelope(reason, session_lang())}\n`)
221
+ process.stdout.write(`${envelope}\n`)
181
222
  }
182
223
 
183
224
  const stop_guard = { outcome_of, stop_outcome_for_payload }
@@ -65,8 +65,12 @@ function rewrite_command_value(value: string): string {
65
65
  .split(PNPM_JOSH_PREFIX)
66
66
  .join(BUNDLE_INVOCATION)
67
67
 
68
- if (!rewritten.includes(claude_plugin_config.MARKETPLACE_PATH)) return rewritten
69
- if (rewritten.includes(MISSING_INSTALL_GUARD)) return rewritten
68
+ if (
69
+ !rewritten.includes(claude_plugin_config.MARKETPLACE_PATH) ||
70
+ rewritten.includes(MISSING_INSTALL_GUARD)
71
+ ) {
72
+ return rewritten
73
+ }
70
74
 
71
75
  return `${MISSING_INSTALL_GUARD}${rewritten}`
72
76
  }
@@ -9,18 +9,23 @@ import { GIT_LOCATION_VARIABLES } from '#scripts/git/git-location-environment'
9
9
  // Every path after the prefix is relative, so a hook fired from a
10
10
  // subdirectory would otherwise miss its script. The root is git's, resolved with the location
11
11
  // variables a git hook exports cleared first, and resolved with them only when that fails — git
12
- // metadata outside the work tree. The consumer rewrite (`hook-command-rewrite.ts`) keeps this prefix,
13
- // so both sides stay one mechanism for Claude Code and Codex alike.
12
+ // metadata outside the work tree. When the cleared lookup succeeds the variables are cleared for the
13
+ // hook too, so its own git calls act on the checkout it stands in. The consumer rewrite
14
+ // (`hook-command-rewrite.ts`) keeps this prefix, so both sides stay one mechanism for Claude Code and
15
+ // Codex alike.
14
16
  //
15
- // **Everything after the root is one script** (`scripts/hooks/run-hook.sh`):
16
- // clearing the location variables for the hook itself, the bundle-ready gate and the live-source
17
- // fallback live there once, so each hook command names only its hook.
17
+ // **A hook runs on every tool call, so its launch spawns as little as it can** (joshuafolkken/kit#3946):
18
+ // the shell's own `unset` clears the variables rather than an `env` process, and that one lookup
19
+ // both finds the root and decides whether the variables stay — `git` starts once.
20
+ //
21
+ // **Everything after the root is one script** (`scripts/hooks/run-hook.sh`): the bundle-ready gate and
22
+ // the live-source fallback live there once, so each hook command names only its hook.
18
23
 
19
24
  const HOOK_DIST_DIR = 'dist/hooks'
20
25
  const RUN_HOOK_SCRIPT = 'scripts/hooks/run-hook.sh'
21
- const UNSET_OPTIONS = GIT_LOCATION_VARIABLES.map((name) => `-u ${name}`).join(' ')
26
+ const LOCATION_VARIABLES = GIT_LOCATION_VARIABLES.join(' ')
22
27
  const ROOT_LOOKUP = 'git rev-parse --show-toplevel'
23
- const PROJECT_ROOT_PREFIX = `project_root="$(env ${UNSET_OPTIONS} ${ROOT_LOOKUP} 2>/dev/null || ${ROOT_LOOKUP})" && cd "$project_root" && `
28
+ const PROJECT_ROOT_PREFIX = `if project_root="$(unset ${LOCATION_VARIABLES}; ${ROOT_LOOKUP} 2>/dev/null)"; then unset ${LOCATION_VARIABLES}; else project_root="$(${ROOT_LOOKUP})"; fi && cd "$project_root" && `
24
29
 
25
30
  // `name` is the hook's bundle under `dist/hooks` without `.js` (e.g. `pretool-guard`), followed by any
26
31
  // arguments it takes (e.g. `codex-hook-adapter pretool`).
@@ -30,8 +30,10 @@ function patch_deploy_vps_pnpm(content: string): string {
30
30
  const with_pnpm12 = content.replaceAll(PNPM_INSTALL_PATTERN, (command) =>
31
31
  replace_outdated_install(command),
32
32
  )
33
- if (with_pnpm12.includes(VERSION_CHECK_MARKER)) return with_pnpm12
34
- if (!OLD_PNPM_CHECK_PATTERN.test(with_pnpm12)) return with_pnpm12
33
+
34
+ if (with_pnpm12.includes(VERSION_CHECK_MARKER) || !OLD_PNPM_CHECK_PATTERN.test(with_pnpm12)) {
35
+ return with_pnpm12
36
+ }
35
37
 
36
38
  return with_pnpm12
37
39
  .replace(OLD_MAJOR_LINE_PATTERN, '')
@@ -76,9 +76,12 @@ function firing_point_problem(body: string): string | undefined {
76
76
  }
77
77
 
78
78
  function baseline_problem(body: string): string | undefined {
79
- if (!markdown_section.has_line(body, BASELINE_HEADING)) return undefined
80
-
81
- if (baseline_measure.has_command_value(body)) return undefined
79
+ if (
80
+ !markdown_section.has_line(body, BASELINE_HEADING) ||
81
+ baseline_measure.has_command_value(body)
82
+ ) {
83
+ return undefined
84
+ }
82
85
 
83
86
  return 'baseline must be written as `command → value`, not prose (it has to be re-runnable)'
84
87
  }
@@ -86,9 +89,12 @@ function baseline_problem(body: string): string | undefined {
86
89
  // The reproduction section is checked for format only where the heading is present — a missing heading
87
90
  // is already reported by `heading_problems`, so reporting it here too would double the message.
88
91
  function reproduction_problem(body: string): string | undefined {
89
- if (!markdown_section.has_line(body, REPRODUCTION_HEADING)) return undefined
90
-
91
- if (reproduction_measure.has_command_output(body)) return undefined
92
+ if (
93
+ !markdown_section.has_line(body, REPRODUCTION_HEADING) ||
94
+ reproduction_measure.has_command_output(body)
95
+ ) {
96
+ return undefined
97
+ }
92
98
 
93
99
  return 'reproduction must be a command and its actual output (a fenced block), not prose ("確認した" is not re-runnable)'
94
100
  }
@@ -0,0 +1,59 @@
1
+ import { markdown_section } from './markdown-section'
2
+
3
+ // joshuafolkken/kit#3909: a run-time-shortening proposal was filed and implemented on a criterion that
4
+ // never weighed how often the speed-up applies against what it costs when it does not. An Issue that
5
+ // declares that aim must therefore state its expected effect — the hit rate, the gain per hit, the loss
6
+ // per miss and where each number came from — before `issue:lint` and `issue:file` accept it. The
7
+ // declaration, the heading and the item labels are the template's, pinned by the document test.
8
+
9
+ const DECLARATION_LINE = '- 目的: 時間短縮'
10
+ const BACKGROUND_HEADING = '## 背景'
11
+ const HEADING = '## 期待効果'
12
+ const ITEM_LABELS: ReadonlyArray<string> = ['効く率', '1 回あたりの得', '外れたときの損', '出典']
13
+ // A template placeholder value — the `<...>` guidance a filed Issue replaces. It is not an estimate, but a
14
+ // value that only opens with `<` (an upper bound such as `<1 min`) is one, so the placeholder must close with `>` too.
15
+ const PLACEHOLDER = /^<.*>$/u
16
+
17
+ // Whether the body declares a run-time-shortening aim under its background heading.
18
+ function is_target(body: string): boolean {
19
+ const background = markdown_section.section_lines(body, BACKGROUND_HEADING).join('\n')
20
+
21
+ return markdown_section.has_unfenced_line(background, DECLARATION_LINE)
22
+ }
23
+
24
+ function item_value(line: string, label: string): string | undefined {
25
+ const prefix = `- ${label}:`
26
+
27
+ if (!line.startsWith(prefix)) return undefined
28
+
29
+ return line.slice(prefix.length).trim()
30
+ }
31
+
32
+ function has_item(lines: ReadonlyArray<string>, label: string): boolean {
33
+ return lines.some((line) => {
34
+ const value = item_value(line.trim(), label)
35
+
36
+ return value !== undefined && value.length > 0 && !PLACEHOLDER.test(value)
37
+ })
38
+ }
39
+
40
+ // Every expected-effect problem a body has, or an empty array. A body without the declaration has none.
41
+ function problems(body: string): ReadonlyArray<string> {
42
+ if (!is_target(body)) return []
43
+ if (!markdown_section.has_line(body, HEADING)) return [`missing heading: ${HEADING}`]
44
+ const section = markdown_section.section_lines(body, HEADING).join('\n')
45
+ const lines = markdown_section.unfenced_lines(section)
46
+
47
+ return ITEM_LABELS.filter((label) => !has_item(lines, label)).map(
48
+ (label) => `missing expected-effect item: - ${label}: <value>`,
49
+ )
50
+ }
51
+
52
+ const expected_effect_lint = {
53
+ DECLARATION_LINE,
54
+ HEADING,
55
+ ITEM_LABELS,
56
+ problems,
57
+ }
58
+
59
+ export { expected_effect_lint }
@@ -101,16 +101,26 @@ async function local_slug_for(targets: ReadonlyArray<CiteTarget>): Promise<strin
101
101
  return has_bare_target ? await git_gh_command.repo_get_name_with_owner() : undefined
102
102
  }
103
103
 
104
+ // The citation lines and the failure lines of one invocation, apart.
105
+ interface CiteReport {
106
+ lines: ReadonlyArray<string>
107
+ failures: ReadonlyArray<string>
108
+ }
109
+
110
+ function split_results(results: ReadonlyArray<LineResult>): CiteReport {
111
+ return {
112
+ lines: results.filter((result) => result.kind === 'ok').map((result) => result.line),
113
+ failures: results.filter((result) => result.kind === 'fail').map((result) => result.line),
114
+ }
115
+ }
116
+
104
117
  // Citations to stdout so the block stays paste-ready; failures to stderr so each unanswered number is
105
118
  // named without landing in what gets pasted. A failure sets a non-zero exit.
106
- function report(results: ReadonlyArray<LineResult>): number {
107
- const lines = results.filter((result) => result.kind === 'ok').map((result) => result.line)
108
- const failures = results.filter((result) => result.kind === 'fail').map((result) => result.line)
109
-
110
- if (lines.length > 0) console.info(lines.join(LINE_SEPARATOR))
111
- for (const failure of failures) console.error(failure)
119
+ function report(cited: CiteReport): number {
120
+ if (cited.lines.length > 0) console.info(cited.lines.join(LINE_SEPARATOR))
121
+ for (const failure of cited.failures) console.error(failure)
112
122
 
113
- return failures.length === 0 ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
123
+ return cited.failures.length === 0 ? SUCCESS_EXIT_CODE : FAILURE_EXIT_CODE
114
124
  }
115
125
 
116
126
  // The targets the command line names, or `undefined` for a line nobody can read — an unknown flag and
@@ -122,14 +132,11 @@ function read_targets(args: ReadonlyArray<string>): ReadonlyArray<CiteTarget> |
122
132
  return parse_targets(parsed.positionals, parsed.values.repo)
123
133
  }
124
134
 
125
- async function run(args: ReadonlyArray<string>): Promise<number> {
135
+ // The lines `issue:cite` prints for `args`, without printing them — the `Stop` hook shows them itself
136
+ // (`stop-actions.ts`). `undefined` for a line nobody can read.
137
+ async function cite(args: ReadonlyArray<string>): Promise<CiteReport | undefined> {
126
138
  const targets = read_targets(args)
127
-
128
- if (targets === undefined) {
129
- console.error(USAGE)
130
-
131
- return FAILURE_EXIT_CODE
132
- }
139
+ if (targets === undefined) return undefined
133
140
 
134
141
  const local_slug = await local_slug_for(targets)
135
142
  const results = await bounded_pool.bounded_map(
@@ -138,7 +145,19 @@ async function run(args: ReadonlyArray<string>): Promise<number> {
138
145
  async (target) => await cite_target(target, local_slug),
139
146
  )
140
147
 
141
- return report(results)
148
+ return split_results(results)
149
+ }
150
+
151
+ async function run(args: ReadonlyArray<string>): Promise<number> {
152
+ const cited = await cite(args)
153
+
154
+ if (cited === undefined) {
155
+ console.error(USAGE)
156
+
157
+ return FAILURE_EXIT_CODE
158
+ }
159
+
160
+ return report(cited)
142
161
  }
143
162
 
144
163
  async function main(args: ReadonlyArray<string>): Promise<void> {
@@ -147,6 +166,7 @@ async function main(args: ReadonlyArray<string>): Promise<void> {
147
166
 
148
167
  const issue_cite_cli = {
149
168
  USAGE,
169
+ cite,
150
170
  parse_targets,
151
171
  run,
152
172
  }
@@ -2,7 +2,7 @@ import { issue_bug_label } from './issue-bug-label'
2
2
  import { BREAKING_CHANGE_LABEL, BUG_LABEL, ENHANCEMENT_LABEL, has_label_name } from './issue-labels'
3
3
  import { markdown_section } from './markdown-section'
4
4
 
5
- const FEATURE_DECLARATIONS = ['- 目的: 機能追加', '- 目的: 機能改善']
5
+ const FEATURE_DECLARATIONS = ['- 目的: 機能追加', '- 目的: 機能改善', '- 目的: 時間短縮']
6
6
  const BREAKING_DECLARATION = '- 互換性: 破壊的変更'
7
7
  const NON_BUG_DECLARATION = '- 種別: 非不具合'
8
8
  const CLASSIFICATION_LABELS = [BUG_LABEL, ENHANCEMENT_LABEL, BREAKING_CHANGE_LABEL]
@@ -2,6 +2,7 @@
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { behavior_change_lint } from './behavior-change-lint'
5
+ import { expected_effect_lint } from './expected-effect-lint'
5
6
  import { issue_classification } from './issue-classification'
6
7
  import { issue_lint } from './issue-lint'
7
8
 
@@ -9,7 +10,7 @@ import { issue_lint } from './issue-lint'
9
10
  // one per line, exiting non-zero. It reads a path rather than stdin so it can
10
11
  // lint a body written to a file before the `gh api … issues` call that files it. A body that declares
11
12
  // itself a behavior-change Issue is additionally held to the firing-point, baseline and reproduction
12
- // rules.
13
+ // rules, and one that declares a run-time-shortening aim to the expected-effect estimate.
13
14
 
14
15
  const SUCCESS_EXIT_CODE = 0
15
16
  const FAILURE_EXIT_CODE = 1
@@ -17,14 +18,15 @@ const OK_MESSAGE = 'ok'
17
18
  const NO_LABELS = 'none'
18
19
  const USAGE = 'Usage: josh issue:lint <path-to-issue-body>'
19
20
 
20
- // Every problem a body has: the four base template headings it lacks, then the behavior-change
21
- // problems (empty for an Issue that does not declare itself one).
21
+ // Every problem a body has: the four base template headings it lacks, then the behavior-change and
22
+ // expected-effect problems (each empty for an Issue that does not declare itself one).
22
23
  function problems(body: string): ReadonlyArray<string> {
23
24
  const missing = issue_lint.missing_headings(body).map((heading) => `missing heading: ${heading}`)
24
25
 
25
26
  return [
26
27
  ...missing,
27
28
  ...behavior_change_lint.problems(body),
29
+ ...expected_effect_lint.problems(body),
28
30
  ...issue_classification.problems(body),
29
31
  ]
30
32
  }
@@ -73,8 +73,9 @@ function read_arguments(argv: ReadonlyArray<string>): ScoutArguments | undefined
73
73
  const { values, positionals } = parsed
74
74
  const [title] = positionals
75
75
 
76
- if (has_invalid_arguments(title, values.body, values['body-file'])) return undefined
77
- if (title === undefined) return undefined
76
+ if (title === undefined || has_invalid_arguments(title, values.body, values['body-file'])) {
77
+ return undefined
78
+ }
78
79
 
79
80
  return { title, body: values.body ?? '', body_file: values['body-file'] }
80
81
  }
@@ -102,7 +103,7 @@ function draft_of(args: ScoutArguments, repo: string): BacklogIssue {
102
103
  }
103
104
 
104
105
  // The epic beside a candidate is the placement answer for a draft that cites nothing: the epic half
105
- // below decides from prose references, and a title-only draft has none to give it.
106
+ // below decides from signal references, and a title-only draft has none to give it.
106
107
  function format_duplicate(candidate: DuplicateCandidate): string {
107
108
  const score = candidate.is_referenced === true ? 'ref' : candidate.score.toFixed(SCORE_DIGITS)
108
109
  const epic = candidate.epic === undefined ? '' : ` (epic ${session_cite.issue(candidate.epic)})`
@@ -161,12 +162,13 @@ function format_epic(decision: BundleDecision): Array<string> {
161
162
  return lines
162
163
  }
163
164
 
164
- // A draft cites nothing, so the epic half has nothing to decide from: its two signals are prose
165
- // references and recorded dependencies, and a title carries neither. Saying "file it standalone" there
166
- // reports a scan that found nothing when none was possible — the confident wrong answer the gap
165
+ // A draft cites nothing where a citation counts, so the epic half has nothing to decide from: its two
166
+ // signals are references in the acceptance criteria or the `parent: #N` line and recorded
167
+ // dependencies, and a title or a background mention carries neither. Saying "file it standalone"
168
+ // there reports a scan that found nothing when none was possible — the confident wrong answer the gap
167
169
  // warnings exist to prevent everywhere else in this command.
168
170
  const NO_REFERENCE_LINE =
169
- 'Epic: not asked — no issue number in the summary. Pass --body "…#<N>…", or take the epic printed beside a duplicate above.'
171
+ 'Epic: not asked — no issue number in the summary\'s acceptance criteria or parent line. Pass --body "parent: #<N>", or take the epic printed beside a duplicate above.'
170
172
 
171
173
  // Every placing verdict above asserts that no epic already tracks the draft's relatives, and a cut
172
174
  // epic listing cannot support that: `epic:bundle` withholds them for it,
@@ -194,18 +196,22 @@ function format_epic_decision(
194
196
  return format_epic(decision).join('\n')
195
197
  }
196
198
 
197
- // Whether the summary names an issue at all, read through the same parser the decision itself uses —
198
- // a second reading of "does this body cite anything" could disagree with the one that matters.
199
- function has_reference(draft: BacklogIssue): boolean {
200
- return epic_audit_logic.parse_references(draft.body, draft.repo).length > 0
199
+ // The issue numbers the summary names where `decide_bundle` counts a reference — its acceptance
200
+ // criteria and its `parent: #N` line — read through the same parser the decision itself uses: a
201
+ // second reading of "does this body cite anything" could disagree with the one that matters, and an
202
+ // issue cited only as the background of the filing is not one the draft belongs with.
203
+ function signal_numbers(draft: BacklogIssue): Array<number> {
204
+ return epic_audit_logic
205
+ .signal_references(draft.body, draft.repo)
206
+ .map((reference) => reference.number)
201
207
  }
202
208
 
203
209
  // The open epics the summary names outright. `decide_bundle` excludes an epic from the candidate pool
204
- // — a container is not a sibling — so a draft saying "part of epic #1153", which is the phrasing the
205
- // workflow documents themselves suggest, otherwise reaches `none` and is told to file standalone with
206
- // the epic it named never mentioned.
210
+ // — a container is not a sibling — so a draft whose `parent: #1153` line names the epic, which is the
211
+ // phrasing the workflow documents themselves suggest, otherwise reaches `none` and is told to file
212
+ // standalone with the epic it named never mentioned.
207
213
  function named_epics(draft: BacklogIssue, issues: ReadonlyArray<BacklogIssue>): Array<number> {
208
- const named = new Set(epic_audit_logic.parse_references(draft.body, draft.repo))
214
+ const named = new Set(signal_numbers(draft))
209
215
 
210
216
  return issues
211
217
  .filter((issue) => issue.is_epic === true && named.has(issue.number))
@@ -240,7 +246,7 @@ function format_epic_answer(
240
246
 
241
247
  if (named.length > 0) return format_named_epics(named, decision).join('\n')
242
248
 
243
- return format_epic_decision(decision, has_reference(draft), is_membership_established)
249
+ return format_epic_decision(decision, signal_numbers(draft).length > 0, is_membership_established)
244
250
  }
245
251
 
246
252
  // The closed rows reach the duplicate half only. `decide_bundle` places a draft among issues that
@@ -3,6 +3,9 @@
3
3
  // way, so the slicing lives in one place rather than being written twice.
4
4
 
5
5
  const HEADING_PREFIX = '## '
6
+ // A body saved from GitHub's web editor ends its lines with CRLF; a kept `\r` would end every line's
7
+ // text with it and defeat each `$`-anchored match downstream.
8
+ const LINE_BREAK_PATTERN = /\r?\n/u
6
9
 
7
10
  function is_heading(line: string): boolean {
8
11
  return line.trimStart().startsWith(HEADING_PREFIX)
@@ -29,21 +32,31 @@ function next_fence(line: string, fence: string): string | undefined {
29
32
  return undefined
30
33
  }
31
34
 
32
- function unfenced_lines(body: string): ReadonlyArray<string> {
35
+ function lines_of(body: string): ReadonlyArray<string> {
36
+ return body.split(LINE_BREAK_PATTERN)
37
+ }
38
+
39
+ // The body's lines with every fenced line, fences included, blanked. Indentation is kept, so a list
40
+ // item's indented continuation survives.
41
+ function fence_free_lines(body: string): ReadonlyArray<string> {
33
42
  let fence = ''
34
43
  const visible: Array<string> = []
35
44
 
36
- for (const line of body.split('\n')) {
45
+ for (const line of lines_of(body)) {
37
46
  const next = next_fence(line, fence)
38
- const visible_line = line.replace(/^(?: {4}|\t).*/u, '')
39
47
 
40
- visible.push(next === undefined && fence === '' ? visible_line : '')
48
+ visible.push(next === undefined && fence === '' ? line : '')
41
49
  fence = next ?? fence
42
50
  }
43
51
 
44
52
  return visible
45
53
  }
46
54
 
55
+ // As `fence_free_lines`, with an indented code line (4 spaces or a tab) blanked too.
56
+ function unfenced_lines(body: string): ReadonlyArray<string> {
57
+ return fence_free_lines(body).map((line) => line.replace(/^(?: {4}|\t).*/u, ''))
58
+ }
59
+
47
60
  function has_unfenced_line(body: string, text: string): boolean {
48
61
  return unfenced_lines(body).some((line) => line.trim() === text)
49
62
  }
@@ -59,7 +72,7 @@ function section_lines_matching(
59
72
  body: string,
60
73
  is_target: (line: string) => boolean,
61
74
  ): ReadonlyArray<string> {
62
- const lines = body.split('\n')
75
+ const lines = lines_of(body)
63
76
  const visible = unfenced_lines(body)
64
77
  const start = visible.findIndex((line) => is_target(line))
65
78
 
@@ -80,6 +93,7 @@ function section_lines(body: string, heading: string): ReadonlyArray<string> {
80
93
  const markdown_section = {
81
94
  section_lines,
82
95
  section_lines_matching,
96
+ fence_free_lines,
83
97
  unfenced_lines,
84
98
  has_line,
85
99
  has_unfenced_line,