@joshuafolkken/kit 1.1455.0 → 1.1471.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 (211) hide show
  1. package/.claude/skills/dependency-update/SKILL.md +26 -55
  2. package/.claude/skills/epic-commands/SKILL.md +87 -148
  3. package/.claude/skills/epic-commands/epic-bundle.md +45 -77
  4. package/.claude/skills/epic-commands/execution-waves.md +44 -81
  5. package/.claude/skills/verify-ui/SKILL.md +25 -50
  6. package/.claude/skills/workflow-commands/background-commands.md +36 -47
  7. package/.claude/skills/workflow-commands/backlogrun-child.md +72 -85
  8. package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
  9. package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
  10. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
  11. package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
  12. package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
  13. package/.claude/skills/workflow-commands/chain-rule.md +24 -34
  14. package/.claude/skills/workflow-commands/followup.md +57 -76
  15. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  16. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  17. package/.claude/skills/workflow-commands/observation-filing.md +62 -107
  18. package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
  19. package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
  20. package/.claude/skills/workflow-commands/prerequisite.md +10 -15
  21. package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
  22. package/.claude/skills/workflow-commands/retrospective.md +31 -50
  23. package/.claude/skills/workflow-commands/split-assessment.md +34 -58
  24. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
  25. package/CLAUDE.md +6 -5
  26. package/dist/CLAUDE.md +6 -5
  27. package/dist/hooks/batch-guard.js +6 -6
  28. package/dist/hooks/{chunk-7WQBPLUN.js → chunk-43ZHT22P.js} +31 -15
  29. package/dist/hooks/{chunk-LIAFCXL7.js → chunk-AS74OU3D.js} +23 -7
  30. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
  31. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
  32. package/dist/hooks/{chunk-WNLVGT7E.js → chunk-H5HVWYXK.js} +4 -4
  33. package/dist/hooks/{chunk-HHI6FO3R.js → chunk-IIGFYXKC.js} +345 -240
  34. package/dist/hooks/{chunk-GPLGZ6IC.js → chunk-J47HU5H5.js} +14 -3
  35. package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
  36. package/dist/hooks/{chunk-D5QBTOCZ.js → chunk-K4LMBQC2.js} +4 -4
  37. package/dist/hooks/{chunk-TBJFTSUY.js → chunk-KC5AK2GF.js} +8 -6
  38. package/dist/hooks/chunk-SHGPANFB.js +56 -0
  39. package/dist/hooks/{chunk-5H4SSDSD.js → chunk-YHBZAJSV.js} +3 -3
  40. package/dist/hooks/{chunk-HFGWYGUV.js → chunk-YYPI7SJZ.js} +5 -5
  41. package/dist/hooks/codex-hook-adapter.js +13 -13
  42. package/dist/hooks/duplicate-read-guard.js +7 -7
  43. package/dist/hooks/format-edited.js +3 -3
  44. package/dist/hooks/inputs.json +216 -214
  45. package/dist/hooks/investigation-guard.js +7 -7
  46. package/dist/hooks/pretool-guard.js +12 -12
  47. package/dist/hooks/session-lang.js +6 -5
  48. package/dist/hooks/stop-guard.js +18 -14
  49. package/dist/josh.js +5 -6
  50. package/dist/managed-marker/index.js +1 -2
  51. package/dist/version/index.js +15 -12
  52. package/eslint/base.js +2 -1
  53. package/eslint/rules/unicorn.js +16 -0
  54. package/package.json +3 -3
  55. package/prompts/coding-standards.md +12 -22
  56. package/prompts/collaboration-workflow/file-edits.md +26 -44
  57. package/prompts/collaboration-workflow/gh-rest.md +20 -36
  58. package/prompts/collaboration-workflow/glossary.md +5 -7
  59. package/prompts/collaboration-workflow/issue-citation.md +7 -13
  60. package/prompts/collaboration-workflow/issue-template.md +29 -29
  61. package/prompts/collaboration-workflow/operating-rules.md +35 -44
  62. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  63. package/prompts/collaboration-workflow/overview.md +14 -19
  64. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  65. package/prompts/collaboration-workflow/principles.md +50 -53
  66. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  67. package/prompts/collaboration-workflow/report-format.md +32 -49
  68. package/prompts/collaboration-workflow/residency.md +10 -10
  69. package/prompts/collaboration-workflow/rule-delivery.md +9 -9
  70. package/prompts/collaboration-workflow/shell-body.md +10 -20
  71. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  72. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  73. package/prompts/collaboration-workflow/wip-cap.md +24 -30
  74. package/prompts/collaboration-workflow.md +24 -25
  75. package/prompts/refactoring.md +33 -43
  76. package/prompts/review-rubric.md +84 -129
  77. package/prompts/review.md +111 -141
  78. package/prompts/sonar-hotspot-handling.md +22 -50
  79. package/prompts/testing-guide.md +48 -99
  80. package/scripts/adopt/adopt-toolkits.ts +2 -4
  81. package/scripts/agent/agent-exit-record.ts +23 -1
  82. package/scripts/agent/codex-agent-argv.ts +7 -2
  83. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  84. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  85. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  86. package/scripts/backlog/backlog-drive-named.ts +2 -4
  87. package/scripts/backlog/backlog-drive-restore.ts +19 -1
  88. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  89. package/scripts/backlog/backlog-drive.ts +11 -6
  90. package/scripts/backlog/backlog-ready.ts +13 -6
  91. package/scripts/behavior/behavior-rules.ts +3 -2
  92. package/scripts/cost-runtime/codex-usage.ts +4 -2
  93. package/scripts/cost-runtime/cost-cli.ts +1 -2
  94. package/scripts/delegation/investigation-guard.ts +1 -1
  95. package/scripts/delegation/investigation-reads.ts +1 -1
  96. package/scripts/discovery/repo-discovery.ts +4 -2
  97. package/scripts/discovery/repo-origin.ts +2 -4
  98. package/scripts/doctor/doctor-logic.ts +6 -3
  99. package/scripts/document/document-byte-budget.ts +27 -27
  100. package/scripts/document/document-byte-check.ts +1 -2
  101. package/scripts/document/entry-read-budget.ts +2 -2
  102. package/scripts/epic/epic-audit-rationale.ts +8 -2
  103. package/scripts/epic/epic-bundle-cli.ts +1 -2
  104. package/scripts/epic/epic-chains.ts +6 -2
  105. package/scripts/epic/epic-close-comment.ts +1 -2
  106. package/scripts/gate/verification-gate.ts +7 -2
  107. package/scripts/gh/git-pr.ts +6 -2
  108. package/scripts/git/git-common-directory.ts +3 -3
  109. package/scripts/git/git-diff-reads.ts +16 -9
  110. package/scripts/git/git-execa-error.ts +1 -2
  111. package/scripts/hooks/format-edited-file.ts +10 -4
  112. package/scripts/hooks/pre-push-unit.ts +15 -4
  113. package/scripts/hooks/pretool-guard.ts +23 -29
  114. package/scripts/hooks/step-zero-notice.ts +3 -2
  115. package/scripts/init/hook-command-rewrite.ts +6 -2
  116. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  117. package/scripts/issue/behavior-change-lint.ts +12 -6
  118. package/scripts/issue/issue-scout-cli.ts +3 -2
  119. package/scripts/josh/file-map-stamp.ts +1 -3
  120. package/scripts/josh/hook-decision.ts +17 -2
  121. package/scripts/lane/lane-await.ts +3 -2
  122. package/scripts/lane/lane-child-invocation.ts +14 -1
  123. package/scripts/lane/lane-guard-policy.ts +4 -4
  124. package/scripts/lane/lane-ledger.ts +9 -0
  125. package/scripts/lane/lane-load.ts +9 -2
  126. package/scripts/lane/lane-measure-cli.ts +2 -0
  127. package/scripts/lane/lane-parked.ts +18 -0
  128. package/scripts/lane/lane-process-load.ts +125 -0
  129. package/scripts/lane/lane-process-stats.ts +93 -0
  130. package/scripts/lane/lane-registry.ts +8 -3
  131. package/scripts/lane/lane-session-broker.ts +29 -5
  132. package/scripts/lane/lane-spent.ts +14 -3
  133. package/scripts/lane/lane-stats.ts +1 -0
  134. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  135. package/scripts/overrides/lockfile-overrides.ts +1 -3
  136. package/scripts/propagate/propagate.ts +1 -2
  137. package/scripts/release/release-progress.ts +3 -2
  138. package/scripts/report/report-lint.ts +7 -4
  139. package/scripts/review/review-record-cli.ts +6 -2
  140. package/scripts/rules/destructive-command.ts +6 -9
  141. package/scripts/rules/direct-filing.ts +3 -7
  142. package/scripts/rules/early-heartbeat.ts +7 -18
  143. package/scripts/rules/file-body.ts +6 -13
  144. package/scripts/rules/filing-cap.ts +3 -6
  145. package/scripts/rules/git-force.ts +5 -9
  146. package/scripts/rules/implementation-cut.ts +37 -35
  147. package/scripts/rules/index-guard.ts +4 -7
  148. package/scripts/rules/issue-comments.ts +5 -15
  149. package/scripts/rules/josh-git-bare.ts +3 -6
  150. package/scripts/rules/lane-background.ts +10 -21
  151. package/scripts/rules/lane-carry-conflict.ts +7 -17
  152. package/scripts/rules/lane-interactive-ask.ts +6 -11
  153. package/scripts/rules/lane-park.ts +5 -13
  154. package/scripts/rules/lane-split-park.ts +2 -7
  155. package/scripts/rules/lane-switch-main.ts +9 -12
  156. package/scripts/rules/oracle-consulted.ts +4 -5
  157. package/scripts/rules/piped-verification.ts +81 -31
  158. package/scripts/rules/poll-loop.ts +7 -17
  159. package/scripts/rules/pre-gate-cut.ts +7 -21
  160. package/scripts/rules/prior-comment-read.ts +7 -5
  161. package/scripts/rules/protected-files.ts +5 -9
  162. package/scripts/rules/raw-field-body.ts +3 -7
  163. package/scripts/rules/rule-body-guard.ts +4 -11
  164. package/scripts/rules/run-tail-rule.ts +6 -22
  165. package/scripts/rules/shell-body-trigger.ts +4 -6
  166. package/scripts/rules/shell-segments.ts +7 -0
  167. package/scripts/rules/stop-rules.ts +16 -29
  168. package/scripts/rules/test-declared-commit.ts +4 -10
  169. package/scripts/rules/third-party-write.ts +5 -10
  170. package/scripts/rules/worktree-guard.ts +5 -12
  171. package/scripts/run/board/run-board-github.ts +11 -3
  172. package/scripts/run/board/run-board-idle.ts +31 -0
  173. package/scripts/run/board/run-board-read.ts +9 -12
  174. package/scripts/run/board/run-board-reason.ts +7 -3
  175. package/scripts/run/board/run-board-state.ts +4 -0
  176. package/scripts/run/board/run-board-status.ts +18 -3
  177. package/scripts/run/board/run-board-thanks.ts +7 -2
  178. package/scripts/run/board/run-board-tick.ts +32 -7
  179. package/scripts/run/carry/run-carry-args.ts +1 -3
  180. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  181. package/scripts/run/cut/run-cut-cli.ts +15 -20
  182. package/scripts/run/cut/run-cut-decline.ts +72 -0
  183. package/scripts/run/cut/run-cut-ready.ts +74 -0
  184. package/scripts/run/cut/run-cut-report.ts +13 -0
  185. package/scripts/run/cut/run-cut.ts +10 -1
  186. package/scripts/run/hold/run-hold-cli.ts +14 -3
  187. package/scripts/run/hold/run-hold.ts +6 -3
  188. package/scripts/run/merge/run-merge-cli.ts +30 -7
  189. package/scripts/run/merge/run-merge-ship.ts +59 -1
  190. package/scripts/run/merge/run-merge.ts +3 -2
  191. package/scripts/run/run-ending.ts +31 -3
  192. package/scripts/run/run-parent-cut-hook.ts +5 -10
  193. package/scripts/run/run-preflight.ts +1 -2
  194. package/scripts/run/run-stop-notify.ts +1 -2
  195. package/scripts/run/run-watcher-guard.ts +6 -3
  196. package/scripts/run/ship/run-ship-review-steps.ts +13 -4
  197. package/scripts/run/ship/run-ship-review.ts +15 -2
  198. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  199. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  200. package/scripts/safe-chain/project-config.ts +7 -2
  201. package/scripts/sync/sync-hook-safety.ts +8 -2
  202. package/scripts/test/unit-worker-share.ts +3 -2
  203. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  204. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  205. package/scripts/time-runtime/time-shell.ts +10 -0
  206. package/scripts/version/effective-upstream.ts +3 -4
  207. package/scripts/version/kit-version-config.ts +8 -2
  208. package/scripts/version/latest-corepack.ts +1 -2
  209. package/scripts/version/release-hold.ts +11 -4
  210. package/scripts/version/version-commands.ts +12 -4
  211. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -61,8 +61,12 @@ const FLAG_PREFIX = '-'
61
61
  function switch_target(segment: string): string | undefined {
62
62
  const call = git_argv.parse(segment)
63
63
 
64
- if (call?.subcommand !== SWITCH_SUBCOMMAND) return undefined
65
- if (call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))) return undefined
64
+ if (
65
+ call?.subcommand !== SWITCH_SUBCOMMAND ||
66
+ call.args.some((argument) => NON_COLLIDING_FLAGS.has(argument))
67
+ ) {
68
+ return undefined
69
+ }
66
70
 
67
71
  return call.args.find((argument) => !argument.startsWith(FLAG_PREFIX))
68
72
  }
@@ -103,16 +107,9 @@ function is_switch_away_from_lane(
103
107
  // prohibits. Apostrophes are avoided so the single-quoted literal needs no escaping, as the siblings do.
104
108
  const LANE_SWITCH_MAIN_REASON =
105
109
  '⛔ lane child on `git switch main`: this session is a dispatched lane child, and switching to the ' +
106
- 'default branch here always fails — a lane is a linked work tree, the primary checkout holds the ' +
107
- 'default branch, and git allows one branch in one work tree at a time (`fatal: main is already used ' +
108
- 'by worktree at <the primary checkout>`, measured 11 times in the 2026-09-21 backlogrun). Do not ' +
109
- 'switch: the lane is already on its own issue branch, branched from a fresh default before it opened, ' +
110
- 'and the latest default is brought in during the gate by `pnpm josh main:merge`. So skip this step ' +
111
- 'and keep implementing on the current branch — no `git switch`, no `git pull`. `pnpm josh main:sync` ' +
112
- '(`josh ms`) is refused in a lane for the same reason, and a lane is finished with `pnpm josh ' +
113
- 'lane:close <issue-number>`. The parent runs `pnpm josh ms` in the primary checkout ' +
114
- 'before each lane opens, never the child — `backlogrun-lanes.md` and `backlogrun-child.md` are the ' +
115
- 'single sources. This rule fires on every occurrence, not once per run.'
110
+ 'default branch here always fails (`already used by worktree`: the primary checkout holds it). Skip ' +
111
+ 'this step and keep implementing on the current branch; `pnpm josh main:merge` brings the default ' +
112
+ 'in during the gate. Refused on every occurrence (`backlogrun-lanes.md`, `backlogrun-child.md`).'
116
113
 
117
114
  // The row itself, so `delivered-rules.ts` spreads one entry. `is_trigger` reads the input through
118
115
  // `on_bash_command` — the `Bash` tool-name gate every command row shares — and `decide` returns true so
@@ -48,11 +48,10 @@ function command_line(oracle: Oracle): string {
48
48
  // the single source of the procedure.
49
49
  function reason_for(oracle: Oracle, firing_point: FiringPoint): string {
50
50
  return (
51
- `⛔ decision oracle not consulted: this is ${firing_point.describes}, but the run has not run ` +
52
- `\`${command_line(oracle)}\` first — the oracle that decides ${oracle.decision.toLowerCase()}. ` +
53
- `It answers ${oracle.vocabulary.join(' | ')}; single source ${oracle.single_source}. Reissue ` +
54
- `after running it and reading its verdict. Every such call is refused until that command is on ` +
55
- `the transcript.`
51
+ `⛔ decision oracle not consulted: this is ${firing_point.describes}. Run ` +
52
+ `\`${command_line(oracle)}\` first — it decides ${oracle.decision.toLowerCase()} and answers ` +
53
+ `${oracle.vocabulary.join(' | ')} (${oracle.single_source}). Every such call is refused until ` +
54
+ `that command is on the transcript.`
56
55
  )
57
56
  }
58
57
 
@@ -73,20 +73,16 @@ function is_masked_verification(command: string): boolean {
73
73
  return time_shell.discarded_commands(command).some((segment) => is_verification_command(segment))
74
74
  }
75
75
 
76
- // The masking, the two ways out of it and the boundary, in the shape a refusal can carry. **The way
76
+ // The masking and the ways out of it, in the shape a refusal can carry; the boundary is the topic file's. **The way
77
77
  // out travels with the refusal rather than being named**, because a delivery saying only "do not pipe
78
78
  // it" leaves the caller with the same long output and no sanctioned way to read it, which is what put
79
79
  // the pipe there in the first place.
80
80
  const PIPED_VERIFICATION_REASON =
81
81
  "⛔ piped verification: a pipeline exits with its last command's status, so `pnpm josh gate | " +
82
- 'tail` reports success on a gate that failed, and the verdict is discarded before anything reads ' +
83
- 'it. Run the check without the pipe — josh prints its verdict line last, so the harness output cap ' +
84
- 'keeps it even when the middle is elided. Where the output genuinely has to be narrowed, redirect ' +
85
- 'it to a file and read ranges from that file, or prefix `set -o pipefail` so the pipeline carries ' +
86
- "the check's status. Read the printed verdict either way, never the exit code alone. Read-only " +
87
- 'listings are untouched — this fires only on a command whose result means pass or fail. The rule ' +
88
- 'is in `prompts/collaboration-workflow/output-bounds.md`. Reissue this call with no pipe — it ' +
89
- 'fires once per run and cannot repeat on the call in hand.'
82
+ 'tail` reports success on a gate that failed. Reissue this call with no pipe (josh prints its ' +
83
+ 'verdict last), or redirect it to a file and read ranges from that file, or prefix ' +
84
+ '`set -o pipefail`; read the verdict line, never the exit code alone ' +
85
+ '(`prompts/collaboration-workflow/output-bounds.md`).'
90
86
 
91
87
  // Every command position on the line: the chain cut `shell_segments` makes, each piece of a pipeline
92
88
  // inside it taken on its own.
@@ -129,26 +125,34 @@ function keeps_verdict_intact(command: string): boolean {
129
125
  // `grep` stopping at its first match — closes the pipe on a check still writing, and under `pipefail` the
130
126
  // check's SIGPIPE turns a passing run into exit 141; `head` also cuts the verdict line josh prints last.
131
127
  //
132
- // **Every command on the line is a josh check or a filter after one.** The rewrite answers `allow`, so
133
- // anything else chained beside the check — `&& rm -r dist`, or a `git log | head` never written for
134
- // `pipefail` — would skip the permission prompt it would otherwise meet. Those lines are refused as before.
128
+ // **Each masked check is wrapped on its own, and the rest of the line runs as typed.** Most of the
129
+ // refusals left after #3570 were a check chained beside other work — `vitest …; pnpm josh lint:related
130
+ // | tail -5; git status` — so the check's own pipeline becomes `(set -o pipefail; …)` and nothing else
131
+ // on the line is put under a `pipefail` it was not written for (joshuafolkken/kit#3875). The envelope
132
+ // carries no permission decision (`hook_decision.flow_rewrite_envelope`), so whatever else the line runs
133
+ // meets its own permission prompt exactly as the typed call would have.
135
134
  //
136
- // **The raw line is read for what the cut cannot see.** `segments_of` does not split on a lone `&`, and
137
- // `time_shell.unquoted` blanks a double-quoted `$(…)`, so `| tail & curl …`, `| tail > ~/.zshrc` and
138
- // `| grep "$(cmd)"` all look like a filter after a check. Any `&`, redirection, backtick or `$(` beyond
139
- // the `&&` chain and the check's own `2>&1` leaves the line to the refusal.
140
- const PIPEFAIL_PREFIX = 'set -o pipefail; '
135
+ // **The check's own pipeline is read for what the cut cannot see.** `parts_of` does not split on a lone
136
+ // `&`, so `| tail & curl …`, `| tail > ~/.zshrc` and `| tail -n $(cmd)` look like a filter after a check.
137
+ // Any `&`, redirection, backtick or `$(` there beyond the check's own `2>&1` leaves the line to the
138
+ // refusal, as does a heredoc anywhere on it, whose body lines the cut would read as commands. An
139
+ // unquoted `(` or `)` does too: the cut splits inside a group, so `(cd x && pnpm josh lint) | tail` would
140
+ // wrap the inner check and leave the group's own pipe without `pipefail`.
141
+ const PIPEFAIL_OPEN = '(set -o pipefail; '
142
+ const PIPEFAIL_CLOSE = ')'
143
+ const COMMENT_START = /(?:^|\s)#/u
141
144
  const FULL_READING_FILTER = /^(?:tail|grep)\b/u
142
145
  const EARLY_EXIT_GREP =
143
146
  /^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u
144
147
 
145
148
  const ALLOWED_OPERATORS = /2>&1|&&/gu
146
149
  const SIDE_EFFECT_SYNTAX = /[&<>`]|\$\(/u
150
+ const GROUP_SYNTAX = /[()]/u
147
151
 
148
152
  const PIPEFAIL_NOTE =
149
- '↻ piped verification: the josh check was piped, so `set -o pipefail;` was prefixed and the call ran ' +
150
- "with the check's status carried to the exit code. Read the printed verdict, never the exit code " +
151
- 'alone — `prompts/collaboration-workflow/output-bounds.md` (joshuafolkken/kit#3570).'
153
+ '↻ piped verification: the josh check was piped, so its pipeline was wrapped in ' +
154
+ "`(set -o pipefail; …)` to carry the check's status, and the rest of the line ran as typed. Read " +
155
+ 'the printed verdict, never the exit code alone — `prompts/collaboration-workflow/output-bounds.md`.'
152
156
 
153
157
  function reads_to_the_end(piece: string): boolean {
154
158
  return FULL_READING_FILTER.test(piece) && !EARLY_EXIT_GREP.test(piece)
@@ -164,23 +168,69 @@ function has_side_effect_syntax(command: string): boolean {
164
168
  return SIDE_EFFECT_SYNTAX.test(command.replaceAll(ALLOWED_OPERATORS, ' '))
165
169
  }
166
170
 
167
- function is_pipefail_rewritable(command: string): boolean {
168
- return (
169
- is_masked_verification(command) &&
170
- !has_side_effect_syntax(command) &&
171
- pipelines_of(command).every((pipeline) => is_filtered_check(pipeline))
172
- )
171
+ // The side-effect syntax is read on the typed part, since a double-quoted `$(cmd)` still runs; the pipe
172
+ // cut and the grouping are read on the blanked one, since a quoted `|` or `(` is text.
173
+ function is_rewritable_part(raw: string, judged: string): boolean {
174
+ const pipeline = judged.split('|').map((piece) => piece.trim())
175
+
176
+ return !has_side_effect_syntax(raw) && !GROUP_SYNTAX.test(judged) && is_filtered_check(pipeline)
177
+ }
178
+
179
+ // Where the part's code ends: an unquoted `#` opens a comment, which would swallow the closing `)`.
180
+ function code_end(judged: string): number {
181
+ const comment = COMMENT_START.exec(judged)
182
+
183
+ return comment === null ? judged.length : comment.index
184
+ }
185
+
186
+ // The part with its surrounding whitespace and any trailing comment kept outside the subshell, so the
187
+ // line reads as typed.
188
+ function wrapped(raw: string, judged: string): string {
189
+ const code = raw.slice(0, code_end(judged))
190
+ const core = code.trim()
191
+ const lead = code.slice(0, code.indexOf(core))
192
+
193
+ return `${lead}${PIPEFAIL_OPEN}${core}${PIPEFAIL_CLOSE}${raw.slice(lead.length + core.length)}`
194
+ }
195
+
196
+ // One part of the line as it will run: as typed where it masks nothing, wrapped where it masks a check a
197
+ // wrapper can carry, and `undefined` where it masks one no wrapper can.
198
+ function rewritten_part(raw: string, judged: string): string | undefined {
199
+ if (!is_masked_verification(judged)) return raw
200
+
201
+ return is_rewritable_part(raw, judged) ? wrapped(raw, judged) : undefined
173
202
  }
174
203
 
175
- // The call's input with the prefix in front, or `undefined` where the call is not a rewritable masking.
204
+ // The command with every masked check wrapped, judged on the quote-blanked line and cut from the typed
205
+ // one at the same offsets — a `|` inside `grep -E "a|b"` is text, and the typed line keeps its quotes.
206
+ function pipefail_command(command: string): string | undefined {
207
+ const judged = time_shell.blanked(command)
208
+
209
+ if (judged === undefined || !is_masked_verification(command)) return undefined
210
+
211
+ const offset = { at: 0 }
212
+ const parts = shell_segments.parts_of(judged).map((part) => {
213
+ const raw = command.slice(offset.at, offset.at + part.length)
214
+
215
+ offset.at += part.length
216
+
217
+ return rewritten_part(raw, part)
218
+ })
219
+
220
+ const rewritten = parts.includes(undefined) ? undefined : parts.join('')
221
+
222
+ // A line masked as a whole but in no part wraps nothing, so no note may claim a wrap.
223
+ return rewritten === command ? undefined : rewritten
224
+ }
225
+
226
+ // The call's input with each masked check wrapped, or `undefined` where the call is not a rewritable
227
+ // masking.
176
228
  function pipefail_input(call: GuardedCall): Record<string, unknown> | undefined {
177
229
  if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return undefined
178
230
 
179
- const command = time_shell.bash_command(call.input)
180
-
181
- if (!is_pipefail_rewritable(command)) return undefined
231
+ const command = pipefail_command(time_shell.bash_command(call.input))
182
232
 
183
- return { ...call.input, command: `${PIPEFAIL_PREFIX}${command}` }
233
+ return command === undefined ? undefined : { ...call.input, command }
184
234
  }
185
235
 
186
236
  const piped_verification = {
@@ -83,25 +83,15 @@ function is_poll_loop(command: string): boolean {
83
83
  return LOOP_KEYWORD.test(plain) && SLEEP_COMMAND.test(plain)
84
84
  }
85
85
 
86
- // The instruction in the shape a refusal can carry: what the loop is about to cost, why the command's
87
- // exit is the thing to wait on, and the one route that delivers that exit. The measurement is named
88
- // because it is what makes the rule believable — six lanes, about 45 minutes, 13% of their wall clock.
86
+ // The instruction in the shape a refusal can carry: why the command's exit is the thing to wait on,
87
+ // and the one route that delivers that exit. The measurement behind it is in
88
+ // `docs/maintainers/rule-delivery-rationale.md`.
89
89
  const POLL_LOOP_REASON =
90
90
  "⛔ output poll loop: this hand-writes a `while`/`until` wait loop over a command's output, a " +
91
- 'marker file or a process — with a `sleep` or, worse, without one — and the loop does not know ' +
92
- 'when the command finished. A regex that never matches waits to ' +
93
- 'the harness block limit; one that matches a mid-run line exits too early — either way the ' +
94
- 'command exit is not what ends the wait, and each pass spends a full-context round trip ' +
95
- '(joshuafolkken/kit#2371 measured about 45 minutes, 13% of six lanes, lost to exactly this). Do ' +
96
- 'not poll the output file. A command known to take minutes — `pnpm josh gate`, `pnpm josh ' +
97
- 'followup`, `pnpm josh format` — is started detached from the first with `run_in_background` set, ' +
98
- 'and the harness re-invokes you when it exits: its completion notification is what ends the wait, ' +
99
- 'on the task exit rather than a regex or the block limit. So issue the long command in the ' +
100
- 'background and end the turn — do not arm a loop; the wake-up brings you back. To glance at ' +
101
- 'interim output, `Read` the output file once, never in a sleep loop. If you are waiting on CI, ' +
102
- '`pnpm josh followup` waits on it for you. The procedure is ' +
103
- '`.claude/skills/workflow-commands/background-commands.md`, "A command that can take minutes is ' +
104
- 'issued in the background". **This rule fires on every occurrence, not once per run.**'
91
+ 'marker file or a process, and the loop does not know when the command finished. Issue the long ' +
92
+ 'command with `run_in_background` set and end the turn — its exit re-invokes you; for CI, ' +
93
+ '`pnpm josh followup` (`.claude/skills/workflow-commands/background-commands.md`). It fires on ' +
94
+ 'every occurrence.'
105
95
 
106
96
  // The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
107
97
  // occurrence; it declares no `keeps` — waiting on the task exit is the *absence* of a loop, not a
@@ -133,9 +133,7 @@ function dispatched_issue(state: LaneCutState): string | undefined {
133
133
  function uncut_lane_issue(state: LaneCutState): string | undefined {
134
134
  const issue = lane_paths.lane_issue_of(state.directory)
135
135
 
136
- if (issue === undefined) return undefined
137
-
138
- if (dispatched_issue(state) !== issue) return undefined
136
+ if (issue === undefined || dispatched_issue(state) !== issue) return undefined
139
137
 
140
138
  return state.carried()?.issue === issue ? undefined : issue
141
139
  }
@@ -159,26 +157,14 @@ function is_uncut_gate(command: string, state: LaneCutState = current_state()):
159
157
  return warrants_the_cut(state.context_verdict())
160
158
  }
161
159
 
162
- // The instruction in the shape a refusal can carry: what the cut is for, what each verdict means, and
163
- // the reissue sentence every delivery needs. **The verdicts are spelled out rather than pointed at**,
164
- // because `cut` is the only one that ends the turn and a run told merely to "take the cut" would have
165
- // to go and read which of the other five leave it holding the run — the same reason the WIP cap's
166
- // delivery names its three interrupt tests instead of naming the file they live in.
160
+ // The instruction in the shape a refusal can carry: the cut, the one verdict that ends the turn, and
161
+ // the reissue sentence every delivery needs. `cut` is the only verdict that ends the turn, so the
162
+ // refusal says so rather than leaving a run to look up which of the others leave it holding the run.
167
163
  const PRE_GATE_CUT_REASON =
168
164
  '⛔ pre-gate cut: this checkout is a lane dispatched for this issue and the gate is the boundary to ' +
169
- 'cut at, so take the cut before running it. `pnpm josh run:cut <N>` ends this process and ' +
170
- 'relaunches a fresh one that resumes from the gate, dropping the thinking built up while ' +
171
- 'implementing rather than carrying it on every later request — joshuafolkken/kit#1839 measured ' +
172
- '176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step ' +
173
- 'itself firing 0 times in 6 lane children while it was carried as prose. Issue ' +
174
- '`pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** — the ' +
175
- 'fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, ' +
176
- '`busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next ' +
177
- 'call. Never relaunch a second process after `busy`. This fired because the dispatch mark names ' +
178
- 'this lane; a person working here carries no mark and sees no refusal, so there is no ' +
179
- 'human-or-child judgement left to make. The procedure is ' +
180
- '`.claude/skills/workflow-commands/pre-gate-cut.md`. Reissue the gate once the cut has answered — ' +
181
- 'this fires once per run, so it cannot repeat on the call in hand.'
165
+ 'cut at. Issue `pnpm josh run:cut <N>`: on `cut`, end the turn immediately — no Telegram is owed; ' +
166
+ 'on any other verdict (`not-a-lane`, `busy`, …), reissue the gate — this fires once per run ' +
167
+ '(`.claude/skills/workflow-commands/pre-gate-cut.md`).'
182
168
 
183
169
  // **The pre-gate cut is two acts, one call apart**, so its `rule:value`
184
170
  // denominator is only the runs that reached the boundary: a run that issues the cut's entry check and,
@@ -15,20 +15,22 @@ import { tail_commands } from './tail-commands'
15
15
  // **It reads comments in the josh spelling too.** `pnpm josh issue:read <N>` prints the body and every
16
16
  // comment in one call, so a run that opened an Issue with it has read the comments as surely as one
17
17
  // that passed `--comments` — and that spelling is the one the workflow itself reaches for, so leaving
18
- // it out would refuse the reader that obeyed the rule most directly.
18
+ // it out would refuse the reader that obeyed the rule most directly. **`run:entry <N>` and `run:prep <N>`
19
+ // count the same way**: both print the bundled report that carries the body and every comment, and a
20
+ // run that opened with one was refused for a read it had made (joshuafolkken/kit#3875).
19
21
 
20
22
  // The path segment `…/issues/<N>` and `…/issues/<N>/comments` both name the Issue in the same capture.
21
23
  const ISSUE_PATH_NUMBER = /issues\/(\d+)/gu
22
24
  // The positional after `gh issue view`, past any flags that precede it.
23
25
  const ISSUE_VIEW_NUMBER = /\bissue\s+view\s+(?:-{1,2}\S+\s+)*(\d+)/u
24
- // The run of numbers `pnpm josh issue:read` takes as its positional arguments.
25
- const ISSUE_READ_ARGUMENTS = /\bissue:read\b([\s\d]*)/u
26
+ // The run of numbers a josh comment-reading command takes as its positional arguments.
27
+ const ISSUE_READ_ARGUMENTS = /\b(?:issue:read|run:entry|run:prep)\b([\s\d]*)/u
26
28
 
27
29
  // A command that prints an Issue's comments: the comments endpoint, `gh issue view … --comments`, or
28
- // the josh command that reads body and comments together.
30
+ // a josh command that reads body and comments together.
29
31
  const COMMENTS_ENDPOINT = /issues\/\d+\/comments\b/u
30
32
  const VIEW_WITH_COMMENTS = /\bissue\s+view\b[^\n]*--comments\b/u
31
- const ISSUE_READ_COMMAND = /\bissue:read\b/u
33
+ const ISSUE_READ_COMMAND = /\b(?:issue:read|run:entry|run:prep)\b/u
32
34
 
33
35
  function add_issue_read_numbers(command: string, numbers: Set<number>): void {
34
36
  const match = ISSUE_READ_ARGUMENTS.exec(command)
@@ -119,21 +119,17 @@ function is_protected_file_call(
119
119
  }
120
120
 
121
121
  const PROTECTED_FILE_REASON =
122
- '⛔ protected file: `.env` holds the Telegram bot token, and a `.env.*` variant or `.dev.vars` ' +
123
- 'holds the same kind of secret, so reading one — with the Read tool or a shell reader such as ' +
124
- '`cat` / `grep` / `source` — puts a secret in the transcript — the scripts that need it load it ' +
125
- 'in their own process (the tracked `.env.example` and `.env.test` stay readable). ' +
126
- '`.claude/settings.json` in a ' +
127
- 'consumer repository is the deny list and the hooks every other rule rests on, so a run must not ' +
128
- 'rewrite it; `josh sync` owns it, and a change belongs upstream in kit (`CLAUDE.md` → "Route ' +
129
- 'distributed-doc / config changes upstream to kit"). Leave these files alone, or ask the user. ' +
130
- '**This rule fires on every occurrence, not once per run.**'
122
+ '⛔ protected file: `.env`, a `.env.*` variant and `.dev.vars` hold secrets, and a consumer ' +
123
+ '`.claude/settings.json` is owned by `josh sync` (a change belongs upstream in kit). Leave these ' +
124
+ 'files alone, or ask the user. It fires on every occurrence.'
131
125
 
132
126
  const ROW = {
133
127
  id: 'protected-file',
134
128
  is_trigger: is_protected_file_call,
135
129
  reason: PROTECTED_FILE_REASON,
136
130
  decide: (): boolean => true,
131
+ // The opening the reason carried before it was cut to the action and the next command.
132
+ former_reasons: ['⛔ protected file: `.env` holds the Telegram bot token'],
137
133
  }
138
134
 
139
135
  const protected_files = { ROW, is_protected_file_call }
@@ -35,13 +35,9 @@ function posts_at_path_literally(command: string): boolean {
35
35
  // it reads as unbelievable — the call succeeds and returns a URL while posting the path as text.
36
36
  const RAW_FIELD_BODY_REASON =
37
37
  '⛔ literal @path body: this passes `body=@<path>` to `gh api` with a raw field flag (`-f` / ' +
38
- '`--raw-field`), which sends the value verbatim — the string `@<path>` is posted as the comment, ' +
39
- 'not the file it points at. `-F` / `--field` is the flag that reads the file, and the two spellings ' +
40
- 'differ by one character while `gh` exits 0 and returns a comment URL either way, so the break is ' +
41
- 'invisible until a person reads the issue (joshuafolkken/kit#2304 lost two park comments this way). ' +
42
- 'Post an issue or PR comment with `pnpm josh issue:comment <N> --body-file <path>` — the one ' +
43
- 'spelling, which cannot take the wrong flag. The rule and the safe forms are in ' +
44
- '`prompts/collaboration-workflow/shell-body.md`. This rule fires on every occurrence, not once per run.'
38
+ '`--raw-field`), which posts the string `@<path>` rather than the file. Post the comment with ' +
39
+ '`pnpm josh issue:comment <N> --body-file <path>`; this rule fires on every occurrence ' +
40
+ '(`prompts/collaboration-workflow/shell-body.md`).'
45
41
 
46
42
  const ROW = {
47
43
  id: 'raw-field-body',
@@ -131,17 +131,10 @@ function writes_rule_prose(
131
131
  // skips, the commands that answer them, and the reissue once they have run.
132
132
  const RULE_BODY_REASON =
133
133
  '⛔ rule written into prose: this edit adds rule text to CLAUDE.md, a `prompts/**` doc or a skill ' +
134
- 'body. Before it lands, run the residency questions the addition skips. A rule whose answer is ' +
135
- 'computable from mechanically readable inputs should be a decision oracle, not prose — question 0; ' +
136
- '`pnpm josh oracle:list` lists the existing oracles a new one joins (single source ' +
137
- '`scripts/rules/decision-oracle.ts`). A rule that decides *when* or *in what order* to act belongs ' +
138
- 'in the run driver, not prose — the ordering question; `pnpm josh run:step` is the state machine a ' +
139
- 'sequencing rule moves into. Only a rule that is neither — not computable, not ordering — stays ' +
140
- 'prose, and then as a trigger-plus-pointer line rather than a second copy of its procedure. The ' +
141
- 'criterion is `prompts/collaboration-workflow/residency.md` (question 0 and the ordering question); ' +
142
- 'the delivery enumeration is `prompts/collaboration-workflow/rule-delivery.md`. Every such edit is ' +
143
- 'refused until both commands are on the transcript — run them, confirm the rule can be neither an ' +
144
- 'oracle nor an ordering step, then reissue this edit.'
134
+ 'body. Answer question 0 (`pnpm josh oracle:list`) and the ordering question (`pnpm josh ' +
135
+ 'run:step`) of `prompts/collaboration-workflow/residency.md`; refused until both commands are on ' +
136
+ 'the transcript, then reissue this as a trigger-plus-pointer line ' +
137
+ '(`prompts/collaboration-workflow/rule-delivery.md`).'
145
138
 
146
139
  // The two placement oracles the residency questions name: question 0 is `pnpm josh oracle:list`, the
147
140
  // ordering question is `pnpm josh run:step` (its degenerate pre-implementation form `run:next` counts
@@ -137,30 +137,14 @@ function decide(): boolean {
137
137
  return true
138
138
  }
139
139
 
140
- // The instruction in the shape a refusal can carry: what the call is about to cost, the reissue that
141
- // avoids it, and the half a run forgets once the push has landed. The measurement is named because it
142
- // is what makes the rule believable — the work was 17 minutes and the run was 45.
140
+ // The instruction in the shape a refusal can carry: the reissue, and the half a run forgets once the
141
+ // push has landed. The measurement behind it is in `docs/maintainers/rule-delivery-rationale.md`.
143
142
  const RUN_TAIL_REASON =
144
143
  '⛔ run tail: this issues the commit-push-PR step in the foreground, where the harness decides ' +
145
- 'when to stop waiting. Reissue it with `run_in_background` set. In `fullrun #1501` this call was ' +
146
- 'given a 900-second timeout, the harness detached it at its own 600-second cap, and nothing read ' +
147
- 'the output for a further 6m28s — 45m03s of wall clock on about 17 minutes of work ' +
148
- '(joshuafolkken/kit#1510). **Never give a foreground call a timeout above the harness cap**: the ' +
149
- 'cap decides, not the number. A backgrounded call re-invokes you when it exits, so the completion ' +
150
- 'notification is what resumes the run. **When it arrives, the turn that reads it goes straight ' +
151
- 'through any branch-2 filing and `pnpm josh epic:bundle` to `pnpm josh followup` — the turn never ' +
152
- 'ends at the push.** CI went green 6m06s before the merge started in that run, and it started ' +
153
- 'only because the person asked; joshuafolkken/kit#1333 had already settled that the merge is ' +
154
- 'issued in the turn the review closes. While the push runs, do the work that writes nothing — ' +
155
- 'drafting the completion body to a file, deciding the branch-2 disposition. `pnpm josh followup` ' +
156
- 'itself stays in the **foreground** — nearly every step after it reads its result. But a tail ' +
157
- 'does follow the merge, measured at 3.0 min and 5.9% of a run (joshuafolkken/kit#1462), so ' +
158
- 'empty it beforehand: the steps that read the merge result stay after `followup`, and the rest ' +
159
- 'are composed in the turn that issues it, bar the one exception background-commands.md names. The ' +
160
- 'procedure is `.claude/skills/workflow-commands/background-commands.md`, "A command that can take ' +
161
- 'minutes is issued in the background". **This rule fires on every foreground push, not once per ' +
162
- 'run**, so ' +
163
- 'reissuing the same call in the foreground will be refused again.'
144
+ 'when to stop waiting. Reissue it with `run_in_background` set; when it exits, go straight on to ' +
145
+ '`pnpm josh followup` in the foreground — the turn never ends at the push ' +
146
+ '(`.claude/skills/workflow-commands/background-commands.md`). It fires on every foreground push, ' +
147
+ 'not once per run.'
164
148
 
165
149
  const run_tail_rule = {
166
150
  RUN_TAIL_REASON,
@@ -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,18 @@ 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
+
23
29
  // **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
24
30
  // in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
25
31
  // matches it as it is. Widening as well would be a second copy of one rule, and the caller that
@@ -36,6 +42,7 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
36
42
 
37
43
  const shell_segments = {
38
44
  is_josh_command,
45
+ parts_of,
39
46
  segments_of,
40
47
  }
41
48
 
@@ -101,25 +101,19 @@ const NO_OUTCOME: StopOutcome = { reason: undefined }
101
101
  // mid-workflow, and the transcript is what says the notify was sent.
102
102
  const STOP_NOTIFY_REASON =
103
103
  '⛔ 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 ' +
104
+ 'Telegram was sent this turn. Send ' +
106
105
  '`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.'
106
+ '--recommended "<choice>" --alternative "<choice>"` (`CLAUDE.md` → "Mid-workflow stop ' +
107
+ 'notification"), then confirm in one line that it was sent — do not repeat your previous reply.'
111
108
 
112
109
  // **A clean tree that still holds is a tree the next run will trample.** `working-tree-hold.md`: a stop that
113
110
  // leaves the tree clean releases the hold; a `halfrun` pre-commit stop and a `needs-human-review` stop
114
111
  // keep it because their tree is dirty, which is why this row is silent whenever the tree is not clean.
115
112
  const HOLD_RELEASE_REASON =
116
113
  '⛔ 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.'
114
+ 'place. Release it with `pnpm josh run:release <N>` (bare for a `new` entry; ' +
115
+ '`working-tree-hold.md`), then confirm in one line that it was released — do not repeat your ' +
116
+ 'previous reply.'
123
117
 
124
118
  // **A refusal that corrects rather than advises**. The bare `#N` is already
125
119
  // on screen and the hook cannot unsay it, so the reason does the one thing that helps the *next* reply:
@@ -149,13 +143,10 @@ function build_citation_reason(references: ReadonlyArray<string>): string {
149
143
  // first**: there the offer is the correct reply, so `needs_filing` is silent.
150
144
  const FILING_OFFER_REASON =
151
145
  '⛔ 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.'
146
+ 'unattended — nobody is there to answer. Run ' +
147
+ '`pnpm josh issue:file "<title>" --body-file <path> --depth <0|1|2>` (Tier A, ' +
148
+ '`observation-filing.md`), then end with the one-line citation of what was filed; do not repeat ' +
149
+ 'your previous reply. If it is not worth filing after all, say so in one line instead.'
159
150
 
160
151
  // **A headless parent's turn-end is its process's end**. Under `claude -p` the
161
152
  // background waits die with the turn, so the lanes lose their parent and nothing re-invokes it. The
@@ -176,12 +167,10 @@ const HEADLESS_REFUSAL_CAP = 3
176
167
  const HEADLESS_WAIT_CALLS = ['"command":"pnpm josh lane:await']
177
168
 
178
169
  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 ' +
170
+ ' this is a `claude -p` `backlogrun` parent with lanes in flight, and ending this turn kills the ' +
171
+ 'process and its background waits. Hand the loop back with ' +
183
172
  '`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").'
173
+ '(`backlogrun-progress.md`).'
185
174
 
186
175
  const HEADLESS_WAIT_REASON = `${HEADLESS_WAIT_MARKER}${HEADLESS_WAIT_BODY}`
187
176
 
@@ -195,11 +184,9 @@ const PERMISSION_PATTERN = /許可|承認|permission|approve|approval|authorizat
195
184
 
196
185
  const LANE_INDEX_REASON =
197
186
  '⛔ 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.'
187
+ 'a person for permission to change the git index. That is Tier A here (`chain-rule.md`): remove any ' +
188
+ 'conflict markers, run `pnpm josh git -y` (it stages and commits, a conflicted merge included) and ' +
189
+ 'continue the run — do not repeat your previous reply.'
203
190
 
204
191
  function asks_index_permission(message: string): boolean {
205
192
  return INDEX_COMMAND_PATTERN.test(message) && PERMISSION_PATTERN.test(message)
@@ -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