@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
@@ -187,12 +187,10 @@ function carries_a_body(command: string): boolean {
187
187
  // (`git-force.ts`, `lane-carry-conflict.ts`).
188
188
  const SHELL_BODY_REASON =
189
189
  '⛔ shell-evaluated body: this command carries a body inline in double quotes, and that body ' +
190
- 'contains a backtick or a `$`, so the shell runs it — the text is executed rather than merely ' +
191
- 'mangled (joshuafolkken/kit#1198). Write the body to a file and pass it by path: `pnpm josh ' +
192
- 'issue:comment <N> --body-file <path>`, `pnpm josh followup --notify-message-file <path>`, or ' +
193
- "`--body-file <path>` wherever offered. `$'…'` quoting is the other safe form; the rule and the " +
194
- "trigger's blind spots are in `prompts/collaboration-workflow/shell-body.md`. Reissue this call " +
195
- 'once the body is in a file — it fires once per run and cannot repeat on the call in hand.'
190
+ 'contains a backtick or a `$`, so the shell executes it. Reissue this call once the body is in a ' +
191
+ 'file, passed by path (`pnpm josh issue:comment <N> --body-file <path>`, ' +
192
+ "`--notify-message-file <path>`), or use `$'…'` quoting " +
193
+ '(`prompts/collaboration-workflow/shell-body.md`).'
196
194
 
197
195
  const shell_body_trigger = {
198
196
  SHELL_BODY_REASON,
@@ -14,12 +14,32 @@ import { time_shell } from '#scripts/time-runtime/time-shell'
14
14
  // because `sleep 1200 &` is an armed timer sent to the background and has to be seen as one. That is
15
15
  // a different question about a different character, not a second copy of this answer.
16
16
  const SEGMENT_SEPARATOR = /&&|\|\||;|\n/u
17
+ const SEGMENT_PARTS = new RegExp(`(${SEGMENT_SEPARATOR.source})`, 'u')
17
18
 
18
19
  /** The line's commands, trimmed. Empty segments are kept: a caller anchoring at `^` rejects them. */
19
20
  function segments_of(command: string): Array<string> {
20
21
  return command.split(SEGMENT_SEPARATOR).map((segment) => segment.trim())
21
22
  }
22
23
 
24
+ /** The same cut, untrimmed and with each separator kept between its commands, so the parts join back to the line. */
25
+ function parts_of(command: string): Array<string> {
26
+ return command.split(SEGMENT_PARTS)
27
+ }
28
+
29
+ /**
30
+ * The line's first command as the shell runs it — cut where the quote-blanked line puts the first
31
+ * separator, so one inside a quoted string does not cut it — or `undefined` for a heredoc line.
32
+ */
33
+ function first_of(command: string): string | undefined {
34
+ const plain = time_shell.blanked(command)
35
+
36
+ if (plain === undefined) return undefined
37
+
38
+ const cut = plain.search(SEGMENT_SEPARATOR)
39
+
40
+ return (cut === -1 ? command : command.slice(0, cut)).trim()
41
+ }
42
+
23
43
  // **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
24
44
  // in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
25
45
  // matches it as it is. Widening as well would be a second copy of one rule, and the caller that
@@ -35,7 +55,9 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
35
55
  }
36
56
 
37
57
  const shell_segments = {
58
+ first_of,
38
59
  is_josh_command,
60
+ parts_of,
39
61
  segments_of,
40
62
  }
41
63
 
@@ -0,0 +1,77 @@
1
+ // What a held stop is waiting on, read from records rather than guessed from prose
2
+ // (joshuafolkken/kit#3961): the decision lines the reply states, and whether the person's own prompt
3
+ // this turn was a question.
4
+ //
5
+ // **The recommended choice is the reply's, never a fixed placeholder.** A `confirmation` reaches a
6
+ // person away from the session, and "read the reply" tells them nothing about what they are asked to
7
+ // decide. The reply names it on a line of its own — `Recommended: <choice>`, with an optional
8
+ // `Alternative: <choice>` — the same two fields `josh notify` takes, so the hook copies them rather
9
+ // than inferring them.
10
+ //
11
+ // **A turn the person opened with a question is answered in the session they are at.** Its stop is
12
+ // the answer, not a pause, so a Telegram saying "confirmation required" only buries the ones that are.
13
+
14
+ interface StopDecision {
15
+ recommended: string
16
+ alternative: string | undefined
17
+ }
18
+
19
+ // A label may close its emphasis before or after the colon: `**Recommended:**`, `**Recommended**:`.
20
+ const RECOMMENDED_LABEL = /^recommended[*_]*:/iu
21
+ const ALTERNATIVE_LABEL = /^alternative[*_]*:/iu
22
+ // The list bullet and the Markdown emphasis a decision line may be written with. Emphasis is
23
+ // stripped only at the edges of the label and the choice: an underscore inside a choice belongs to
24
+ // a `snake_case` name the person is asked to approve.
25
+ const LEADING_EMPHASIS = /^[*_]+/u
26
+ const TRAILING_EMPHASIS = /(?<![*_])[*_]+$/u
27
+ const BULLET = /^[-*]\s+/u
28
+
29
+ // A prompt that ends asking: a question mark, or a Japanese sentence-final question particle, before
30
+ // any trailing full stop or space.
31
+ const QUESTION_ENDING = /(?:[??]|か|かな|かね)[\s。.]*$/u
32
+
33
+ function unwrapped(text: string): string {
34
+ return text.trim().replace(LEADING_EMPHASIS, '').replace(TRAILING_EMPHASIS, '').trim()
35
+ }
36
+
37
+ function bare_line(line: string): string {
38
+ return unwrapped(line.trim().replace(BULLET, ''))
39
+ }
40
+
41
+ // The choice on the reply's first line labelled `label`, or `undefined` when none carries one.
42
+ function choice_of(message: string, label: RegExp): string | undefined {
43
+ const line = message
44
+ .split('\n')
45
+ .map((one) => bare_line(one))
46
+ .find((one) => label.test(one))
47
+ const choice = line === undefined ? undefined : unwrapped(line.replace(label, ''))
48
+
49
+ return choice === '' ? undefined : choice
50
+ }
51
+
52
+ // The decision the reply asks for, or `undefined` when it names no recommended choice.
53
+ function decision_of(message: string): StopDecision | undefined {
54
+ const recommended = choice_of(message, RECOMMENDED_LABEL)
55
+ if (recommended === undefined) return undefined
56
+
57
+ return { recommended, alternative: choice_of(message, ALTERNATIVE_LABEL) }
58
+ }
59
+
60
+ function is_question(prompt: string): boolean {
61
+ return QUESTION_ENDING.test(prompt.trim())
62
+ }
63
+
64
+ // **A held stop with no decision line is sent back once rather than announced.** Either it waits on a
65
+ // person and has to say on what, or it waits on nobody and the notify would be the noise this rule
66
+ // exists to end; only the run can tell which, so the reason hands it both answers.
67
+ const DECISION_REASON =
68
+ '⛔ stop decision: this working tree is held by your run and your reply names no decision. If the ' +
69
+ 'run waits on a person, end the reply with a `Recommended: <choice>` line and, where there is ' +
70
+ 'one, an `Alternative: <choice>` line — the hook sends the `confirmation` Telegram from them. If ' +
71
+ 'it waits on nobody, continue the run, or say in one line that nothing is awaited. Do not repeat ' +
72
+ 'your previous reply.'
73
+
74
+ const stop_decision = { DECISION_REASON, decision_of, is_question }
75
+
76
+ export type { StopDecision }
77
+ export { stop_decision }
@@ -4,6 +4,7 @@ import { filing_offer } from './filing-offer'
4
4
  import { issue_citation } from './issue-citation'
5
5
  import { lane_background } from './lane-background'
6
6
  import { reply_language } from './reply-language'
7
+ import { stop_decision, type StopDecision } from './stop-decision'
7
8
 
8
9
  // The stop-time rules, delivered on the `Stop` hook.
9
10
  //
@@ -19,11 +20,14 @@ import { reply_language } from './reply-language'
19
20
  // permission envelope. The stop guard is a second *entry* on the one foundation, exactly as
20
21
  // `pretool-guard` is one — not a second copy of the plumbing.
21
22
  //
22
- // **Every rule refuses**. Missing the stop notification or leaving a hold
23
- // on a clean tree costs a person a silent wait or a trampled tree; a bare `#N` in the reply reaches
24
- // the person watching but not the model that could fix it, so it too blocks — `{"decision":"block"}`
25
- // is the one channel a `Stop` hook has to the model, and `stop_hook_active` caps a false positive at a
26
- // single wasted turn. The detection is tightened to match (`issue-citation.ts`, `filing-offer.ts`).
23
+ // **A rule whose answer is fixed acts; only a rule that needs judgement refuses** (joshuafolkken/kit#3919).
24
+ // The stop notification, the hold release and the citation lines are each one mechanical operation the
25
+ // hook can run itself, so they come back as `actions` and the stop goes through: a refusal is a whole
26
+ // model turn spent re-reading the context to run a command the hook already knew. `stop-actions.ts`
27
+ // runs them, and only an action that fails falls back to its refusal. The rest — the reply's language,
28
+ // an unattended filing offer, a lane's index request or running task, a headless parent's wait — need
29
+ // the model, so they still block with `{"decision":"block"}`, the one channel a `Stop` hook has to it;
30
+ // `stop_hook_active` caps a false positive at a single wasted turn.
27
31
 
28
32
  const SWITCH_ENV_KEY = 'JOSH_STOP_GUARD'
29
33
  const BLOCK_DECISION = 'block'
@@ -40,14 +44,22 @@ type StopPayload = z.infer<typeof stop_payload_schema>
40
44
 
41
45
  // What one stop looks like to the rules, computed once by the CLI from the payload and the world.
42
46
  interface StopContext {
43
- // The `run:hold` record for this working tree is still in place (`held` or `stale`).
44
- hold_present: boolean
47
+ // The Issue the `run:hold` record for this working tree names (`held` or `stale`) — `new` for an
48
+ // unnumbered entry — or `undefined` when no record is in place.
49
+ hold_issue: string | undefined
50
+ // That record is this session's own run's, or its owner cannot be told (`run-hold-session.ts`). A
51
+ // hold another run took is that run's pause to announce and release, never this session's.
52
+ hold_owned: boolean
53
+ // The person opened this turn with a question: the reply answers someone at the session, not a
54
+ // pause waiting on them (`stop-decision.ts` → `is_question`).
55
+ answers_question: boolean
45
56
  // `git status --porcelain` came back empty.
46
57
  tree_clean: boolean
47
58
  // That record carries a `prrun` stop mark: the run stopped at a green pull
48
59
  // request and keeps the hold over a clean tree on purpose, so a person can look before it merges.
49
60
  prrun_stopped: boolean
50
- // A `confirmation` notify is on this run's transcript tail.
61
+ // A `confirmation` notify is on this run's transcript tail, or the hook's own send is recorded on the
62
+ // hold since the pause began (`run-hold-session.ts` → `is_notified_since`).
51
63
  notified: boolean
52
64
  // The turn's session-facing reply text.
53
65
  message: string
@@ -56,7 +68,8 @@ interface StopContext {
56
68
  // This stop already forced a continuation earlier in the prompt — the loop-breaker Claude Code
57
69
  // documents, so a run that will not comply is not blocked forever.
58
70
  stop_hook_active: boolean
59
- // A pre-gate cut record is in place for this work tree. A dispatched lane child ends its turn at
71
+ // This turn ended at a cut — its record is in place, or this turn relaunched the successor that may
72
+ // already have cleared it (`stop-guard.ts` → `is_cut_stop`). A dispatched lane child ends its turn at
60
73
  // the cut holding the tree by design and hands off to a fresh process (`pre-gate-cut.md`), so that
61
74
  // turn-end is an automatic continuation, not the person-waiting pause the block rules are for.
62
75
  cut_pending: boolean
@@ -90,38 +103,44 @@ interface StopContext {
90
103
  backlog_parent: boolean
91
104
  }
92
105
 
106
+ // The operations the hook runs itself instead of refusing — each carries what it acts on.
107
+ type StopAction =
108
+ | { kind: 'notify'; issue: string; message: string; decision: StopDecision }
109
+ | { kind: 'release'; issue: string }
110
+ | { kind: 'cite'; references: ReadonlyArray<string> }
111
+
112
+ // `reason` refuses the stop; otherwise `actions` run, and `blocks_on_failure` says whether a failed one
113
+ // falls back to its refusal — never past the loop-breaker.
93
114
  interface StopOutcome {
94
115
  reason: string | undefined
116
+ actions: ReadonlyArray<StopAction>
117
+ blocks_on_failure: boolean
95
118
  }
96
119
 
97
- const NO_OUTCOME: StopOutcome = { reason: undefined }
120
+ const NO_OUTCOME: StopOutcome = { reason: undefined, actions: [], blocks_on_failure: false }
98
121
 
99
122
  // **A mid-workflow stop is announced off-screen before it happens.** `CLAUDE.md` → "Mid-workflow stop
100
123
  // notification" requires the `confirmation` Telegram before any pause; the hold is what says a run is
101
- // mid-workflow, and the transcript is what says the notify was sent.
124
+ // mid-workflow, and the transcript is what says the notify was sent. The hook sends it; this reason is
125
+ // only the fallback for a send that failed.
102
126
  const STOP_NOTIFY_REASON =
103
127
  '⛔ mid-workflow stop notification: this working tree is still held by a run and no `confirmation` ' +
104
- 'Telegram was sent this turn, so a person is being left to wait off-screen without knowing the run ' +
105
- 'paused. `CLAUDE.md` → "Mid-workflow stop notification" requires ' +
128
+ 'Telegram was sent this turn. Send ' +
106
129
  '`pnpm josh notify --task-type confirmation --issue-url "<url>" --body=$\'<reason>\' ' +
107
- '--recommended "<choice>" --alternative "<choice>"` before any ' +
108
- 'mid-workflow pause. Send it, then end with a one-line confirmation that it was sent — do not repeat ' +
109
- 'your previous reply. This fires only while the hold is held and no notify is on the transcript, and ' +
110
- '`stop_hook_active` lets a second stop through so a run is never wedged.'
130
+ '--recommended "<choice>" --alternative "<choice>"` (`CLAUDE.md` → "Mid-workflow stop ' +
131
+ 'notification"), then confirm in one line that it was sent — do not repeat your previous reply.'
111
132
 
112
133
  // **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
113
134
  // leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
114
135
  // keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
136
+ // The hook releases it; this reason is only the fallback for a release that failed.
115
137
  const HOLD_RELEASE_REASON =
116
138
  '⛔ working-tree hold not released: this working tree is clean but its `run:hold` record is still in ' +
117
- 'place, so the next run here runs `pnpm josh ms` believing the tree is free while ' +
118
- 'you hold it. The workflow-commands skill → `working-tree-hold.md`: a stop that leaves the tree clean ' +
119
- 'releases the hold with `pnpm josh run:release <N>` (bare for a `new` entry). A `halfrun` ' +
120
- 'pre-commit stop and a `needs-human-review` stop keep the hold because their tree is dirty, and a ' +
121
- '`prrun` stop keeps it by its stop mark — this row is silent there. Release it, then end with a one-line confirmation that it was released — do ' +
122
- 'not repeat your previous reply.'
123
-
124
- // **A refusal that corrects rather than advises**. The bare `#N` is already
139
+ 'place. Release it with `pnpm josh run:release <N>` (bare for a `new` entry; ' +
140
+ '`working-tree-hold.md`), then confirm in one line that it was released — do not repeat your ' +
141
+ 'previous reply.'
142
+
143
+ // **The fallback when the hook could not print the citation lines itself**. The bare `#N` is already
125
144
  // on screen and the hook cannot unsay it, so the reason does the one thing that helps the *next* reply:
126
145
  // it names the numbers it detected and hands over the exact `issue:cite` call that prints the
127
146
  // paste-ready lines, then asks for the corrected citation lines alone — not the whole reply reissued.
@@ -149,13 +168,10 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
149
168
  // first**: there the offer is the correct reply, so `needs_filing` is silent.
150
169
  const FILING_OFFER_REASON =
151
170
  '⛔ filing offer: your reply offers to file an Issue instead of filing it, and this run is ' +
152
- 'unattended — nobody is there to answer. Filing into a first-party repository there is Tier A — ' +
153
- "the trigger is the judgement that it is worth filing, not the run's progress (the " +
154
- 'workflow-commands skill → `observation-filing.md`). Run ' +
155
- '`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` — it scouts, lints, labels and ' +
156
- 'runs `epic:bundle` itself — then end with the one-line citation of what was filed; do not repeat ' +
157
- 'your previous reply. If it is not worth filing ' +
158
- 'after all, say so in one line instead.'
171
+ 'unattended — nobody is there to answer. Run ' +
172
+ '`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` (Tier A, ' +
173
+ '`observation-filing.md`), then end with the one-line citation of what was filed; do not repeat ' +
174
+ 'your previous reply. If it is not worth filing after all, say so in one line instead.'
159
175
 
160
176
  // **A headless parent's turn-end is its process's end**. Under `claude -p` the
161
177
  // background waits die with the turn, so the lanes lose their parent and nothing re-invokes it. The
@@ -176,12 +192,10 @@ const HEADLESS_REFUSAL_CAP = 3
176
192
  const HEADLESS_WAIT_CALLS = ['"command":"pnpm josh lane:await']
177
193
 
178
194
  const HEADLESS_WAIT_BODY =
179
- ' this is a `claude -p` `backlogrun` parent (`JOSH_RUN_HEADLESS`) with lanes still ' +
180
- 'in flight, and ending this turn ends the process — its background waits are killed with it and the ' +
181
- "lanes are left without a parent. Do not watch them yourself: the `run:wake` supervisor's driver " +
182
- 'watches the lanes and wakes a session only for a branch that needs judging. Hand the loop back with ' +
195
+ ' this is a `claude -p` `backlogrun` parent with lanes in flight, and ending this turn kills the ' +
196
+ 'process and its background waits. Hand the loop back with ' +
183
197
  '`pnpm josh run:carry --cut --owner "$PPID"` (or `--end` when the run is over), then end the turn ' +
184
- '(`backlogrun-progress.md` → "Picking the lanes up in the fresh session").'
198
+ '(`backlogrun-progress.md`).'
185
199
 
186
200
  const HEADLESS_WAIT_REASON = `${HEADLESS_WAIT_MARKER}${HEADLESS_WAIT_BODY}`
187
201
 
@@ -195,11 +209,9 @@ const PERMISSION_PATTERN = /許可|承認|permission|approve|approval|authorizat
195
209
 
196
210
  const LANE_INDEX_REASON =
197
211
  '⛔ lane index permission: this is a dispatched lane child (`JOSH_LANE_CHILD`) and your reply asks ' +
198
- "a person for permission to change the git index. A lane is a per-run work tree with no person's " +
199
- 'staging to protect, and the sanctioned commit flow is already authorized: remove any conflict ' +
200
- 'markers, then run `pnpm josh git -y`, which stages and commits them — a conflicted merge included. ' +
201
- 'This is Tier A (`.claude/skills/workflow-commands/chain-rule.md` → "origin/main is merged in before ' +
202
- 'the gate"). Do it and continue the run — do not repeat your previous reply.'
212
+ 'a person for permission to change the git index. That is Tier A here (`chain-rule.md`): remove any ' +
213
+ 'conflict markers, run `pnpm josh git -y` (it stages and commits, a conflicted merge included) and ' +
214
+ 'continue the run — do not repeat your previous reply.'
203
215
 
204
216
  function asks_index_permission(message: string): boolean {
205
217
  return INDEX_COMMAND_PATTERN.test(message) && PERMISSION_PATTERN.test(message)
@@ -259,42 +271,52 @@ function needs_filing(context: StopContext): boolean {
259
271
  //
260
272
  // **A lane handed to the detached ship stands them down too**: the hold now
261
273
  // belongs to the supervisor driving the merge, and the person hears from that supervisor's own notify.
274
+ //
275
+ // **Another run's hold stands them down as well**: the pause and the claim are that run's.
262
276
  function is_held_idle(context: StopContext): boolean {
263
- return context.hold_present && !context.agent_pending && !context.handed_off
277
+ if (context.hold_issue === undefined || !context.hold_owned) return false
278
+
279
+ return !context.agent_pending && !context.handed_off
264
280
  }
265
281
 
282
+ // **A stop that answers the person's question is no pause** (joshuafolkken/kit#3961): they are reading
283
+ // the reply where it was written, whatever the tree's hold says.
266
284
  function needs_notify(context: StopContext): boolean {
267
- return is_held_idle(context) && !context.notified
285
+ return is_held_idle(context) && !context.notified && !context.answers_question
268
286
  }
269
287
 
270
- function needs_release(context: StopContext): boolean {
271
- return is_held_idle(context) && context.tree_clean && !context.prrun_stopped
288
+ // The notify a stop owes but cannot send: its reply names no decision to announce.
289
+ function needs_decision(context: StopContext): boolean {
290
+ return needs_notify(context) && stop_decision.decision_of(context.message) === undefined
272
291
  }
273
292
 
274
- function citation_reason(context: StopContext): string | undefined {
275
- const references = issue_citation.unquoted_references(context.message, context.prompt)
276
- if (references.length === 0) return undefined
277
-
278
- return build_citation_reason(references)
293
+ function needs_release(context: StopContext): boolean {
294
+ return is_held_idle(context) && context.tree_clean && !context.prrun_stopped
279
295
  }
280
296
 
281
- // The two rules on how a reply is written. **A headless agent's reply has no reader**:
282
- // its text lands in a stream-json log, so correcting its citations or its
283
- // language costs a turn and reaches nobody. The artifacts a person does read — a Telegram, an Issue, a
284
- // pull request — are held to the same rules by their own guards, not here.
285
- function wording_reason(context: StopContext): string | undefined {
286
- if (context.headless_agent) return undefined
297
+ // The bare references the citation lines are printed for. **A headless agent's reply has no reader**:
298
+ // its text lands in a stream-json log, so correcting its citations or its language costs a turn and
299
+ // reaches nobody. The artifacts a person does read — a Telegram, an Issue, a pull request — are held
300
+ // to the same rules by their own guards, not here.
301
+ function bare_references(context: StopContext): ReadonlyArray<string> {
302
+ if (context.headless_agent) return []
287
303
 
288
- return citation_reason(context) ?? language_reason(context)
304
+ return issue_citation.unquoted_references(context.message, context.prompt)
289
305
  }
290
306
 
291
- // The three rules that read the reply itself, filing offer first and the language last. A drifted reply
307
+ // The two rules that read the reply and need the model, the filing offer first. A drifted reply
292
308
  // refused for another reason is not rewritten, but the drift still ends: `block_envelope` appends the
293
309
  // session-language note to every refusal, so the turn it continues is written in the session language.
294
310
  function reply_reason(context: StopContext): string | undefined {
295
311
  if (needs_filing(context)) return FILING_OFFER_REASON
312
+ if (context.headless_agent) return undefined
313
+
314
+ return language_reason(context)
315
+ }
296
316
 
297
- return wording_reason(context)
317
+ // Sent back once; past the loop-breaker the run has said nothing is awaited, and nothing is sent.
318
+ function decision_reason(context: StopContext): string | undefined {
319
+ return needs_decision(context) ? stop_decision.DECISION_REASON : undefined
298
320
  }
299
321
 
300
322
  // The headless wait holds past the loop-breaker until the spin bound is reached.
@@ -319,24 +341,56 @@ function needs_background_wait(context: StopContext): boolean {
319
341
  return context.lane_child && context.background_pending
320
342
  }
321
343
 
322
- // The two hold rules, notify ahead of release — behind the lane index route
323
- // and the lane background wait, since the notify they would ask for announces
324
- // a pause the run has no reason to take.
325
- function hold_reason(context: StopContext): string | undefined {
344
+ // The two lane rules that mean the run is not pausing at all — ahead of every action, since the notify
345
+ // would announce a pause the run has no reason to take.
346
+ function lane_reason(context: StopContext): string | undefined {
326
347
  if (needs_index_route(context)) return LANE_INDEX_REASON
327
348
  if (needs_background_wait(context)) return lane_background.LANE_BACKGROUND_STOP_REASON
328
- if (needs_notify(context)) return STOP_NOTIFY_REASON
329
- if (needs_release(context)) return HOLD_RELEASE_REASON
330
349
 
331
350
  return undefined
332
351
  }
333
352
 
334
- // First-wins across every rule. Two things stand every rule down: `stop_hook_active` (the
335
- // loop-breaker, so a run that already got one continuation this prompt may stop) and a pre-gate cut in
336
- // flight (a lane child's automatic turn-end, not a person-waiting pause). The two hold rules keep their
337
- // order ahead of the reply rules, so a stop that both still owes its notify and holds a bare `#N` still
338
- // reports the notify first; the filing offer goes ahead of the citation, since the filing it forces
339
- // produces the citation the reply then needs.
353
+ // The refusals that need the model: the lane rules alone, else the reply's own and the decision
354
+ // together. **Joined, since the loop-breaker grants one continuation**: a decision refusal queued behind
355
+ // a language or filing refusal would never be sent, and its stop would pass with no notify.
356
+ function model_reason(context: StopContext): string | undefined {
357
+ const lane = lane_reason(context)
358
+ if (lane !== undefined) return lane
359
+
360
+ const reasons = [reply_reason(context), decision_reason(context)].filter(
361
+ (reason) => reason !== undefined,
362
+ )
363
+
364
+ return reasons.length === 0 ? undefined : reasons.join('\n\n')
365
+ }
366
+
367
+ // The notify carries the reply's own decision; a reply with none sends nothing (`needs_decision`).
368
+ function notify_actions(context: StopContext, issue: string): Array<StopAction> {
369
+ const decision = stop_decision.decision_of(context.message)
370
+ if (decision === undefined || !needs_notify(context)) return []
371
+
372
+ return [{ kind: 'notify', issue, message: context.message, decision }]
373
+ }
374
+
375
+ function hold_actions(context: StopContext): Array<StopAction> {
376
+ const issue = context.hold_issue
377
+ if (issue === undefined) return []
378
+
379
+ const notify = notify_actions(context, issue)
380
+
381
+ return needs_release(context) ? [...notify, { kind: 'release', issue }] : notify
382
+ }
383
+
384
+ // The actions in the order they run: the notify ahead of the release that ends the hold it reports.
385
+ function stop_actions(context: StopContext): ReadonlyArray<StopAction> {
386
+ const references = bare_references(context)
387
+ const cite: Array<StopAction> = references.length > 0 ? [{ kind: 'cite', references }] : []
388
+
389
+ return [...hold_actions(context), ...cite]
390
+ }
391
+
392
+ // First-wins across the refusals. `stop_hook_active` is the loop-breaker, so a run that already got one
393
+ // continuation this prompt may stop; the lane rules go first, then the filing offer, then the language.
340
394
  //
341
395
  // **The headless wait comes first and outlasts `stop_hook_active` up to its spin bound**.
342
396
  // The loop-breaker exists so a person is not wedged behind a rule the run will not satisfy; a headless
@@ -347,18 +401,49 @@ function block_reason(context: StopContext): string | undefined {
347
401
  if (is_headless_held(context)) return HEADLESS_WAIT_REASON
348
402
  if (context.stop_hook_active || context.cut_pending) return undefined
349
403
 
350
- return hold_reason(context) ?? reply_reason(context)
404
+ return model_reason(context)
405
+ }
406
+
407
+ // **The actions run only on the stop that goes through**, so a refused stop never notifies a pause its
408
+ // continuation then cancels, while the continuation's own stop, past the loop-breaker, still runs them
409
+ // and a failure there no longer refuses. A lane child still waiting on its own work runs none, and a
410
+ // pre-gate cut in flight stands everything but the headless wait down: it is a lane child's automatic
411
+ // turn-end, not a person-waiting pause.
412
+ function runs_actions(context: StopContext, reason: string | undefined): boolean {
413
+ return reason === undefined && !context.cut_pending && lane_reason(context) === undefined
351
414
  }
352
415
 
353
416
  function stop_outcome(context: StopContext): StopOutcome {
354
- return { reason: block_reason(context) }
417
+ const reason = block_reason(context)
418
+ if (!runs_actions(context, reason)) return { ...NO_OUTCOME, reason }
419
+
420
+ const actions = stop_actions(context)
421
+ if (actions.length === 0) return NO_OUTCOME
422
+
423
+ return { reason, actions, blocks_on_failure: !context.stop_hook_active }
424
+ }
425
+
426
+ // The refusal an action falls back to when the hook could not run it.
427
+ function fallback_reason(action: StopAction): string {
428
+ if (action.kind === 'notify') return STOP_NOTIFY_REASON
429
+ if (action.kind === 'release') return HOLD_RELEASE_REASON
430
+
431
+ return build_citation_reason(action.references)
355
432
  }
356
433
 
357
434
  // The documented shape a `Stop` hook blocks with: `reason` is fed back to Claude, which then continues
358
435
  // instead of stopping. Plain stdout is not it — only this envelope holds the stop. Every reason leaves
359
436
  // with the session-language note, so no rule can hand the model an English-only turn.
360
- function block_envelope(reason: string, lang: string): string {
361
- return JSON.stringify({ decision: BLOCK_DECISION, reason: `${reason} ${language_note(lang)}` })
437
+ // `notice` is what the hook already did on this stop, shown to the person alongside.
438
+ function block_envelope(reason: string, lang: string, notice?: string): string {
439
+ const refusal = { decision: BLOCK_DECISION, reason: `${reason} ${language_note(lang)}` }
440
+
441
+ return JSON.stringify(notice === undefined ? refusal : { ...refusal, systemMessage: notice })
442
+ }
443
+
444
+ // The shape a `Stop` hook shows the person a line in without holding the stop: `systemMessage` alone.
445
+ function notice_envelope(notice: string): string {
446
+ return JSON.stringify({ systemMessage: notice })
362
447
  }
363
448
 
364
449
  function parse_stop_payload(raw_payload: string): StopPayload | undefined {
@@ -384,10 +469,12 @@ const stop_rules = {
384
469
  block_envelope,
385
470
  build_language_reason,
386
471
  count_headless_refusals,
472
+ fallback_reason,
387
473
  is_enabled,
474
+ notice_envelope,
388
475
  parse_stop_payload,
389
476
  stop_outcome,
390
477
  }
391
478
 
392
- export type { StopContext, StopOutcome }
479
+ export type { StopAction, StopContext, StopOutcome }
393
480
  export { stop_rules }
@@ -30,6 +30,20 @@ function prior_bash_commands(tail: string): ReadonlyArray<string> {
30
30
  return tail.split('\n').flatMap((line) => bash_commands_of(line))
31
31
  }
32
32
 
33
- const tail_commands = { is_bash_use, prior_bash_commands }
33
+ // **Whether a call `is_call` names answered with the `token` line** — the call and its own result
34
+ // paired off parsed blocks, so a transcript that merely quotes the command or the token does not
35
+ // count. An unreadable text reads as no answer.
36
+ function answered(text: string, is_call: (command: string) => boolean, token: string): boolean {
37
+ const blocks = time_transcript_line.parse_text(text).flatMap((line) => line.blocks)
38
+ const call_ids = new Set(
39
+ blocks
40
+ .filter((block) => is_call(time_shell.bash_command(block.input)))
41
+ .map((block) => block.id),
42
+ )
43
+
44
+ return blocks.some((block) => call_ids.has(block.result_id) && block.token_lines.includes(token))
45
+ }
46
+
47
+ const tail_commands = { answered, is_bash_use, prior_bash_commands }
34
48
 
35
49
  export { tail_commands }
@@ -30,16 +30,10 @@ function is_untested_commit(call: GuardedCall): boolean {
30
30
 
31
31
  const REASON =
32
32
  '⛔ test declared: this commits a runtime code change with no test beside it — `pnpm josh ' +
33
- 'test:declared` reads the working tree as `required` (a changed file that is neither a test — ' +
34
- '`*.test.ts` / `*.e2e.ts` — nor a mechanically-exempt non-runtime path). `CLAUDE.md` → Code ' +
35
- 'Change Rules requires a test for ALL code changes: a bug fix gets a regression test, a refactor ' +
36
- 'gets tests pinning behavior first, logic gets a unit test, an observable UI/timing change gets an ' +
37
- 'E2E. Add the declared test and reissue this commit. **If the change genuinely touches no ' +
38
- 'executable runtime path** it is the pre-approved non-runtime exception — declare it in your Step ' +
39
- '0 work summary with the reason it is exempt (non-executable config, a cosmetic asset swap) and ' +
40
- 'reissue; the exception is a presentation, not a stop. Run `pnpm josh test:declared` to see the ' +
41
- 'files it names. The procedure is `prompts/collaboration-workflow/rule-delivery.md` → ' +
42
- '"配送されている規則". This fires once per run, so reissuing will not repeat on the call in hand.'
33
+ 'test:declared` reads it `required` and names the files. Tests are required for ALL code changes: ' +
34
+ 'add the test and reissue, or, for a change that touches no runtime path, declare the non-runtime ' +
35
+ 'exception in your Step 0 work summary and reissue. It fires once per run (`CLAUDE.md` → Code ' +
36
+ 'Change Rules).'
43
37
 
44
38
  // The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
45
39
  // trigger and reason it already single-sources here. Once per run (the default delivery) for the
@@ -103,16 +103,11 @@ function writes_third_party(
103
103
  // to do instead. The command that computes the party is handed over, because the whole point of this
104
104
  // Issue is that the test is computed rather than judged.
105
105
  const THIRD_PARTY_WRITE_REASON =
106
- '⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session — ' +
107
- 'a tracker we do not own is Tier C, and every write there (Issue, comment, PR, Discussion, review) ' +
108
- 'needs explicit current-turn user instruction; typing the target, or holding a correct diagnosis, ' +
109
- 'is not that instruction (`CLAUDE.md` → "Third-party repositories are Tier C", ' +
110
- '`prompts/collaboration-workflow/upstream-interrupt.md`). Confirm the party with ' +
111
- '`pnpm josh repo:party <owner/repo>` — owner equality, computed not judged. A read (`-X GET`, or no ' +
112
- 'field flag) passes untouched; only a write (`-f`/`-F`/`--field`/`--raw-field`/`--input`, or ' +
113
- '`-X POST|PATCH|PUT|DELETE`) is refused. Record the finding on our own Issue and draft the artifact ' +
114
- 'for a person to post — do not publish it yourself. **This rule fires on every occurrence, not once ' +
115
- 'per run.**'
106
+ '⛔ third-party write: this `gh api` call writes to a repository whose owner is not this session, ' +
107
+ 'which is Tier C and needs explicit current-turn user instruction. Check with ' +
108
+ '`pnpm josh repo:party <owner/repo>`, then record the finding on our own Issue and draft the ' +
109
+ 'artifact for a person to post. This rule fires on every occurrence ' +
110
+ '(`prompts/collaboration-workflow/upstream-interrupt.md`).'
116
111
 
117
112
  // `decide` returns true so it refuses every occurrence; it declares no `keeps` — not writing to
118
113
  // another owner's tracker is the absence of a call, not a call, so the row is reported unmeasured
@@ -124,18 +124,11 @@ function is_unauthorized_worktree_change(command: string): boolean {
124
124
  // and — for a stash — the message form that is allowed. `pnpm josh git` and `pnpm josh stash:pop` are
125
125
  // invisible to this trigger, so a run that uses them is never refused.
126
126
  const WORKTREE_MUTATION_REASON =
127
- '⛔ unauthorized working-tree change: `git checkout -- <path>`, `git restore <path>`, a forced ' +
128
- '`git clean` and an unauthorized `git stash` discard work or mutate the repository-wide stash every work tree ' +
129
- 'shares, and none is on the `.claude/settings.json` deny list (`operating-rules.md`). Do not run ' +
130
- 'them on your own judgement. A commit goes through `pnpm josh git`; a stash pop goes through `pnpm ' +
131
- 'josh stash:pop "<message>"`, never a positional `git stash pop` a shared stack lets another lane ' +
132
- 'divert. A stash push is authorized only inside a documented flow and only with a findable message ' +
133
- '— `git stash push -u -m "<message>"` — which this row allows; a bare `git stash`, a message-less ' +
134
- 'push, or `pop` / `apply` / `drop` is refused. A plan that restores a handed-over stash with `git ' +
135
- 'stash apply` / `git stash pop` is read as `pnpm josh stash:pop "<message>"`: apply it that way, ' +
136
- 'continue, and record the substitution on the Issue — Tier A, never a `needs-decision` park ' +
137
- 'unless the plan says why the owner keeps the stash: then stop and confirm (`operating-rules.md` ' +
138
- '→ "no-self-staging"). **This rule fires on every occurrence, not once per run.**'
127
+ '⛔ unauthorized working-tree change: `git checkout --`, `git restore`, `git clean -f`, ' +
128
+ '`git stash`. A documented flow stashes with `git stash push -u -m "<message>"`; a handed-over ' +
129
+ 'stash is `pnpm josh stash:pop "<message>"`; record the substitution on the Issue (Tier A) unless ' +
130
+ 'the plan says why the owner keeps the stash: then stop and confirm. It fires on every occurrence ' +
131
+ '(`operating-rules.md`).'
139
132
 
140
133
  const ROW = {
141
134
  id: 'worktree-mutation',