@joshuafolkken/kit 1.1471.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 (267) hide show
  1. package/.claude/settings.json +6 -6
  2. package/.claude/skills/epic-commands/SKILL.md +2 -14
  3. package/.claude/skills/epic-commands/epic-bundle.md +9 -5
  4. package/.claude/skills/epic-commands/execution-waves.md +1 -2
  5. package/.claude/skills/verify-ui/SKILL.md +1 -2
  6. package/.claude/skills/workflow-commands/SKILL.md +7 -9
  7. package/.claude/skills/workflow-commands/background-commands.md +3 -7
  8. package/.claude/skills/workflow-commands/backlogrun-child.md +3 -10
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +6 -15
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +2 -8
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +10 -19
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +8 -8
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +2 -10
  14. package/.claude/skills/workflow-commands/backlogrun.md +31 -50
  15. package/.claude/skills/workflow-commands/chain-rule.md +7 -6
  16. package/.claude/skills/workflow-commands/delegation.md +12 -8
  17. package/.claude/skills/workflow-commands/entry-sequence.md +1 -3
  18. package/.claude/skills/workflow-commands/followup.md +3 -6
  19. package/.claude/skills/workflow-commands/fullrun-steps.md +3 -5
  20. package/.claude/skills/workflow-commands/fullrun.md +4 -6
  21. package/.claude/skills/workflow-commands/halfrun.md +3 -7
  22. package/.claude/skills/workflow-commands/into-target.md +1 -2
  23. package/.claude/skills/workflow-commands/issue-comments.md +3 -3
  24. package/.claude/skills/workflow-commands/issue-scout.md +3 -2
  25. package/.claude/skills/workflow-commands/latest-gate.md +2 -6
  26. package/.claude/skills/workflow-commands/needs-human-review.md +2 -5
  27. package/.claude/skills/workflow-commands/observation-filing.md +0 -14
  28. package/.claude/skills/workflow-commands/observation-ledger.md +4 -9
  29. package/.claude/skills/workflow-commands/pre-gate-cut.md +11 -16
  30. package/.claude/skills/workflow-commands/prerequisite.md +1 -2
  31. package/.claude/skills/workflow-commands/progress-watcher.md +2 -7
  32. package/.claude/skills/workflow-commands/prrun.md +3 -5
  33. package/.claude/skills/workflow-commands/retrospective.md +4 -4
  34. package/.claude/skills/workflow-commands/split-assessment.md +1 -3
  35. package/.claude/skills/workflow-commands/target-repository.md +1 -2
  36. package/.claude/skills/workflow-commands/working-tree-hold.md +2 -4
  37. package/.codex/hooks.json +6 -6
  38. package/.github/actions/setup-node/action.yml +1 -1
  39. package/.github/workflows/ci.yml +1 -1
  40. package/.github/workflows/publish.yml +2 -2
  41. package/CLAUDE.md +1 -1
  42. package/cspell/index.yaml +7 -0
  43. package/dist/CLAUDE.md +1 -1
  44. package/dist/commands/document-section-cli.js +67 -34
  45. package/dist/hooks/batch-guard.js +6 -6
  46. package/dist/hooks/chunk-4KKD2UTK.js +66 -0
  47. package/dist/hooks/{chunk-J47HU5H5.js → chunk-6TS2JD6D.js} +3 -3
  48. package/dist/hooks/chunk-7DSBJVFH.js +50 -0
  49. package/dist/hooks/{chunk-YHBZAJSV.js → chunk-A65Y4YOR.js} +3 -3
  50. package/dist/hooks/{chunk-E7OIKX5Z.js → chunk-APUO6U4J.js} +85 -7
  51. package/dist/hooks/{chunk-IX6TTTI6.js → chunk-EFXE6C37.js} +8 -22
  52. package/dist/hooks/chunk-GP4IEC4O.js +20 -0
  53. package/dist/hooks/{chunk-SHGPANFB.js → chunk-JP45SOCJ.js} +3 -3
  54. package/dist/hooks/{chunk-K4EOONYM.js → chunk-LAOV7NLM.js} +1 -1
  55. package/dist/hooks/chunk-M4HGZV7U.js +83 -0
  56. package/dist/hooks/{chunk-KC5AK2GF.js → chunk-O6GDRNJB.js} +3 -3
  57. package/dist/hooks/{chunk-YYPI7SJZ.js → chunk-PF2NZL2A.js} +5 -5
  58. package/dist/hooks/{chunk-H5HVWYXK.js → chunk-PTICX4OL.js} +3 -4
  59. package/dist/hooks/{chunk-IIGFYXKC.js → chunk-RT27QRJW.js} +581 -1438
  60. package/dist/hooks/{chunk-43ZHT22P.js → chunk-SYOTDYCI.js} +1127 -1089
  61. package/dist/hooks/{chunk-AS74OU3D.js → chunk-TNUMY6NL.js} +1507 -1336
  62. package/dist/hooks/chunk-UPUZF3AN.js +279 -0
  63. package/dist/hooks/{chunk-EUMACUK2.js → chunk-WK6SMBIC.js} +1948 -806
  64. package/dist/hooks/codex-hook-adapter.js +17 -14
  65. package/dist/hooks/duplicate-read-guard.js +7 -7
  66. package/dist/hooks/format-edited.js +5 -4
  67. package/dist/hooks/inputs.json +317 -264
  68. package/dist/hooks/investigation-guard.js +8 -11
  69. package/dist/hooks/pretool-guard.js +16 -13
  70. package/dist/hooks/session-lang.js +370 -25
  71. package/dist/hooks/stop-guard.js +2576 -176
  72. package/dist/josh.js +2124 -277
  73. package/lefthook/base.yml +5 -3
  74. package/package.json +12 -10
  75. package/prompts/collaboration-workflow/file-edits.md +1 -3
  76. package/prompts/collaboration-workflow/gh-rest.md +1 -2
  77. package/prompts/collaboration-workflow/issue-citation.md +2 -4
  78. package/prompts/collaboration-workflow/issue-template.md +10 -2
  79. package/prompts/collaboration-workflow/lane-instructions.md +35 -0
  80. package/prompts/collaboration-workflow/operating-rules.md +2 -6
  81. package/prompts/collaboration-workflow/output-bounds.md +3 -3
  82. package/prompts/collaboration-workflow/overview.md +1 -1
  83. package/prompts/collaboration-workflow/principles.md +14 -8
  84. package/prompts/collaboration-workflow/report-format.md +9 -0
  85. package/prompts/collaboration-workflow/residency.md +12 -8
  86. package/prompts/collaboration-workflow/rule-delivery.md +3 -11
  87. package/prompts/collaboration-workflow/shell-body.md +6 -6
  88. package/prompts/collaboration-workflow/turn-batching.md +5 -5
  89. package/prompts/collaboration-workflow/upstream-interrupt.md +2 -2
  90. package/prompts/collaboration-workflow/wip-cap.md +6 -6
  91. package/prompts/collaboration-workflow.md +1 -0
  92. package/prompts/review.md +4 -5
  93. package/prompts/testing-guide.md +2 -2
  94. package/scripts/agent/agent-argv.ts +20 -4
  95. package/scripts/agent/agent-role-profile.ts +101 -45
  96. package/scripts/agent/agent-safe-text.ts +60 -0
  97. package/scripts/agent/claude-agent-argv.ts +14 -0
  98. package/scripts/agent/codex-agent-argv.ts +2 -0
  99. package/scripts/agent/lane-instructions.ts +144 -0
  100. package/scripts/backlog/backlog-budget.ts +4 -4
  101. package/scripts/backlog/backlog-drive-named.ts +1 -1
  102. package/scripts/backlog/backlog-drive-restore.ts +9 -1
  103. package/scripts/backlog/backlog-occupied.ts +56 -0
  104. package/scripts/backlog/backlog-ready.ts +47 -10
  105. package/scripts/delegation/duplicate-read-guard.ts +2 -2
  106. package/scripts/delegation/investigation-guard.ts +15 -65
  107. package/scripts/delegation/investigation-reads.ts +0 -1
  108. package/scripts/document/document-byte-budget.ts +6 -5
  109. package/scripts/document/document-section-cli.ts +17 -5
  110. package/scripts/document/entry-read-budget.ts +2 -2
  111. package/scripts/document/read-set-cli.ts +4 -1
  112. package/scripts/epic/epic-audit-orphans.ts +5 -8
  113. package/scripts/epic/epic-audit.ts +52 -1
  114. package/scripts/epic/epic-bundle-evidence.ts +49 -26
  115. package/scripts/epic/epic-bundle-referenced.ts +14 -3
  116. package/scripts/epic/epic-bundle.ts +17 -9
  117. package/scripts/followup/git-followup-workflow.ts +19 -1
  118. package/scripts/gate/check-inputs.ts +129 -0
  119. package/scripts/gate/check-reuse.ts +162 -0
  120. package/scripts/gate/core-budget.ts +93 -44
  121. package/scripts/gate/core-wait.ts +130 -0
  122. package/scripts/gate/free-cores.ts +49 -0
  123. package/scripts/gate/gate-ledger.ts +17 -3
  124. package/scripts/gate/gate-plan.ts +23 -1
  125. package/scripts/gate/gate-report.ts +15 -2
  126. package/scripts/gate/gate-run-steps.ts +150 -0
  127. package/scripts/gate/held-mark.ts +55 -0
  128. package/scripts/gate/machine-capacity.ts +70 -11
  129. package/scripts/gate/pre-commit-type-check.ts +14 -1
  130. package/scripts/gate/verification-gate.ts +58 -125
  131. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  132. package/scripts/gh/git-gh-issue-write.ts +24 -2
  133. package/scripts/gh/git-pr.ts +13 -1
  134. package/scripts/git/main-merge-regenerate.ts +78 -0
  135. package/scripts/git/main-merge.ts +14 -5
  136. package/scripts/hooks/batch-guard.ts +5 -8
  137. package/scripts/hooks/pre-commit-lint.ts +91 -0
  138. package/scripts/hooks/pre-push-unit.ts +1 -1
  139. package/scripts/hooks/pretool-guard.ts +27 -52
  140. package/scripts/hooks/reserved-run.ts +1 -0
  141. package/scripts/hooks/run-hook.sh +10 -12
  142. package/scripts/hooks/stop-actions.ts +204 -0
  143. package/scripts/hooks/stop-guard.ts +62 -21
  144. package/scripts/init/hook-launch.ts +12 -7
  145. package/scripts/issue/expected-effect-lint.ts +59 -0
  146. package/scripts/issue/issue-cite-cli.ts +35 -15
  147. package/scripts/issue/issue-classification.ts +1 -1
  148. package/scripts/issue/issue-lint-cli.ts +5 -3
  149. package/scripts/issue/issue-scout-cli.ts +19 -14
  150. package/scripts/issue/markdown-section.ts +19 -5
  151. package/scripts/josh/command-usage.ts +110 -0
  152. package/scripts/josh/josh-command-map.ts +2 -0
  153. package/scripts/josh/josh-command-types.ts +55 -9
  154. package/scripts/josh/josh-commands-ai.ts +13 -0
  155. package/scripts/josh/josh-commands-development.ts +1 -1
  156. package/scripts/josh/josh-commands-hooks.ts +6 -0
  157. package/scripts/josh/josh-commands-lane.ts +10 -1
  158. package/scripts/josh/josh-commands-workflow.ts +5 -0
  159. package/scripts/josh/josh-logic.ts +6 -15
  160. package/scripts/josh/josh-usage-automation.ts +121 -0
  161. package/scripts/josh/josh-usage-backlog.ts +180 -0
  162. package/scripts/josh/josh-usage-developer.ts +230 -0
  163. package/scripts/josh/josh-usage-hooks.ts +38 -0
  164. package/scripts/josh/josh-usage-issue.ts +205 -0
  165. package/scripts/josh/josh-usage-review.ts +101 -0
  166. package/scripts/josh/josh-usage-run.ts +252 -0
  167. package/scripts/josh/josh-usage-session.ts +238 -0
  168. package/scripts/josh/josh-usage-workflow.ts +212 -0
  169. package/scripts/josh/josh.ts +14 -5
  170. package/scripts/josh/package-directory.ts +20 -0
  171. package/scripts/lane/lane-capacity.ts +8 -2
  172. package/scripts/lane/lane-check-stats.ts +56 -0
  173. package/scripts/lane/lane-child-invocation.ts +35 -4
  174. package/scripts/lane/lane-dispatch.ts +3 -3
  175. package/scripts/lane/lane-guard-policy.ts +2 -2
  176. package/scripts/lane/lane-handoff.ts +8 -1
  177. package/scripts/lane/lane-install.ts +12 -4
  178. package/scripts/lane/lane-ledger.ts +43 -1
  179. package/scripts/lane/lane-limit-cli.ts +8 -9
  180. package/scripts/lane/lane-measure-cli.ts +2 -0
  181. package/scripts/lane/lane-preserve.ts +29 -1
  182. package/scripts/lane/lane-registry.ts +9 -7
  183. package/scripts/lane/lane-restart.ts +5 -26
  184. package/scripts/lane/lane-session-action.ts +146 -0
  185. package/scripts/lane/lane-session-broker.ts +57 -138
  186. package/scripts/lane/lane-withdraw-cli.ts +71 -0
  187. package/scripts/lane/lane-withdraw.ts +182 -0
  188. package/scripts/lint/format.ts +4 -4
  189. package/scripts/lint/lint-parallel.ts +10 -3
  190. package/scripts/lint/lint-related-scope.ts +4 -1
  191. package/scripts/notify/telegram-test.ts +10 -5
  192. package/scripts/report/report-format-reference.ts +2 -0
  193. package/scripts/review/acceptance-map-check.ts +61 -0
  194. package/scripts/review/acceptance-map.ts +205 -0
  195. package/scripts/review/review-brief-cli.ts +1 -1
  196. package/scripts/rules/agent-model.ts +59 -0
  197. package/scripts/rules/delivered-rules.ts +11 -2
  198. package/scripts/rules/filing-cap.ts +1 -0
  199. package/scripts/rules/firing-point.ts +6 -5
  200. package/scripts/rules/guard-remedy.ts +117 -0
  201. package/scripts/rules/issue-comments.ts +67 -6
  202. package/scripts/rules/last-prompt.ts +50 -5
  203. package/scripts/rules/oracle-list-cli.ts +7 -0
  204. package/scripts/rules/piped-verification.ts +3 -1
  205. package/scripts/rules/rule-list.ts +4 -1
  206. package/scripts/rules/rule-registry-rows.ts +10 -0
  207. package/scripts/rules/rule-registry.ts +7 -4
  208. package/scripts/rules/rule-rewrite.ts +114 -0
  209. package/scripts/rules/rule-value.ts +7 -1
  210. package/scripts/rules/shell-segments.ts +15 -0
  211. package/scripts/rules/stop-decision.ts +77 -0
  212. package/scripts/rules/stop-rules.ts +147 -47
  213. package/scripts/rules/tail-commands.ts +15 -1
  214. package/scripts/run/board/run-board-cli.ts +2 -0
  215. package/scripts/run/board/run-board-closed.ts +8 -2
  216. package/scripts/run/board/run-board-github.ts +9 -1
  217. package/scripts/run/board/run-board-header.ts +4 -1
  218. package/scripts/run/board/run-board-labels.ts +2 -0
  219. package/scripts/run/board/run-board-layout.ts +1 -1
  220. package/scripts/run/board/run-board-read.ts +16 -0
  221. package/scripts/run/board/run-board-reason.ts +10 -1
  222. package/scripts/run/board/run-board-render-legend.ts +1 -0
  223. package/scripts/run/board/run-board-render-slot.ts +1 -1
  224. package/scripts/run/board/run-board-render-waits.ts +41 -0
  225. package/scripts/run/board/run-board-render.ts +6 -1
  226. package/scripts/run/board/run-board-row-yield.ts +1 -0
  227. package/scripts/run/board/run-board-state.ts +5 -0
  228. package/scripts/run/board/run-board-status.ts +72 -21
  229. package/scripts/run/board/run-board-tick.ts +5 -1
  230. package/scripts/run/cut/run-cut-cli.ts +3 -2
  231. package/scripts/run/cut/run-cut-report.ts +16 -0
  232. package/scripts/run/detached-launch.ts +23 -4
  233. package/scripts/run/entry/run-entry-cli.ts +8 -1
  234. package/scripts/run/event/run-event-render.ts +1 -0
  235. package/scripts/run/event/run-event-scope.ts +19 -1
  236. package/scripts/run/event/run-event-stream-emit.ts +13 -6
  237. package/scripts/run/event/run-event-stream.ts +4 -0
  238. package/scripts/run/hold/run-hold-session.ts +46 -0
  239. package/scripts/run/hold/run-hold.ts +7 -1
  240. package/scripts/run/launch-priority.ts +32 -0
  241. package/scripts/run/merge/run-merge-cli.ts +16 -65
  242. package/scripts/run/merge/run-merge-signals.ts +99 -0
  243. package/scripts/run/merge/run-merge-steps.ts +11 -1
  244. package/scripts/run/merge/run-merge.ts +21 -9
  245. package/scripts/run/run-liveness.ts +1 -1
  246. package/scripts/run/run-poll-hook.ts +157 -0
  247. package/scripts/run/run-step-cli.ts +22 -15
  248. package/scripts/run/run-step-section.ts +85 -0
  249. package/scripts/run/ship/run-ship-cli.ts +5 -12
  250. package/scripts/run/ship/run-ship-detach.ts +9 -0
  251. package/scripts/run/ship/run-ship-next.ts +10 -2
  252. package/scripts/run/ship/run-ship-overlap.ts +195 -0
  253. package/scripts/run/ship/run-ship-pre-detach.ts +33 -15
  254. package/scripts/run/ship/run-ship-preflight.ts +28 -2
  255. package/scripts/run/ship/run-ship-return.ts +31 -7
  256. package/scripts/run/ship/run-ship-review-steps.ts +12 -10
  257. package/scripts/run/ship/run-ship-steps.ts +1 -1
  258. package/scripts/run/ship/run-ship-stop-context.ts +206 -0
  259. package/scripts/run/ship/run-ship-sync.ts +14 -6
  260. package/scripts/test/test-e2e-guard.ts +30 -5
  261. package/scripts/test/test-unit-guard.ts +16 -1
  262. package/scripts/test/unit-worker-share.ts +26 -4
  263. package/scripts/time-runtime/time-shell.ts +29 -4
  264. package/scripts/time-runtime/time-spans.ts +1 -0
  265. package/scripts/time-runtime/time-tool-call.ts +7 -0
  266. package/templates/gitignore +6 -2
  267. package/dist/hooks/chunk-K4LMBQC2.js +0 -68
@@ -1,3 +1,7 @@
1
+ import { cost_blocks } from '#scripts/cost-runtime/cost-blocks'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import type { GuardedCall } from '#scripts/time-runtime/time-batch-guard'
4
+ import { time_shell } from '#scripts/time-runtime/time-shell'
1
5
  import { bash_triggers } from './bash-triggers'
2
6
  import { gh_api } from './gh-api'
3
7
  import { prior_comment_read } from './prior-comment-read'
@@ -77,6 +81,9 @@ function reads_issue_comments(command: string): boolean {
77
81
  return shell_segments.segments_of(command).some((segment) => fetches_issue_comments(segment))
78
82
  }
79
83
 
84
+ const RULE_DOC = '`.claude/skills/workflow-commands/issue-comments.md`'
85
+ const COMMENTS_FALLBACK = 'gh api repos/{owner}/{repo}/issues/<N>/comments'
86
+
80
87
  // **The refusal hands over the command that fixes it**, because reading is not the same as
81
88
  // obeying: a sentence saying "also read the comments" is prose that moves nothing, while a refused
82
89
  // body read leaves the run holding the reissue that makes the comments *present*. The conflict rule
@@ -88,12 +95,62 @@ function reads_issue_comments(command: string): boolean {
88
95
  // being the Issue the refused call named — so a run that reads only the opening still leaves holding
89
96
  // the fix. The label keeps `time_transcript_line.guard_from_refusal` naming this guard, not the command.
90
97
  const ISSUE_COMMENTS_REASON =
91
- '⛔ issue comments: pnpm josh issue:read <N>\n' +
92
- "Run that in place of this read — an Issue's comments are part of it, and a later comment " +
93
- 'supersedes the body it contradicts; refused until that command is on the transcript. Without ' +
94
- 'josh, read ' +
95
- '`gh api repos/{owner}/{repo}/issues/<N>/comments` beside the body ' +
96
- '(`.claude/skills/workflow-commands/issue-comments.md`).'
98
+ `⛔ issue comments: pnpm josh issue:read <N>\n` +
99
+ `Run that in place of this read — an Issue's comments are part of it, and a later comment ` +
100
+ `supersedes the body it contradicts; refused until that command is on the transcript. Without ` +
101
+ `josh, read \`${COMMENTS_FALLBACK}\` beside the body (${RULE_DOC}).`
102
+
103
+ // **A `gh api` body read is rewritten rather than refused** (joshuafolkken/kit#3920): the fix is the
104
+ // same Issue's comments endpoint, read on the same line, and the path the call already names is all
105
+ // it needs. Prepended on lines of their own, so a trailing comment or a `&&` chain on the typed line
106
+ // cannot swallow or skip them, and the typed line stays last — its exit status is still the call's.
107
+ // A `gh issue view` read is left to the refusal — its global flags (`--repo`) and its `--json`
108
+ // projection have no one comments spelling to add.
109
+ const COMMENTS_JQ = String.raw`'.[] | "— \(.user.login) \(.created_at)\n\(.body)\n"'`
110
+
111
+ const COMMENTS_NOTE =
112
+ "↻ issue comments: this read returned only the Issue's body, so the Issue's comments were fetched " +
113
+ `on the same line — a later comment supersedes the body it contradicts (${RULE_DOC}).`
114
+
115
+ // **Only a line that opens with the body read is rewritten.** Prepended ahead of a `cd` or a
116
+ // subshell, the comments read would resolve `{owner}/{repo}` in another directory and print another
117
+ // repository's comments; and the line is cut on its quote-blanked reading, so a body-read spelling
118
+ // quoted in a string or a heredoc body is never prepended as a real call. Every other line keeps the
119
+ // refusal.
120
+ function rewritable_path(command: string): string | undefined {
121
+ const read = shell_segments.first_of(command)
122
+
123
+ if (read === undefined || ISSUE_VIEW_COMMAND.test(read)) return undefined
124
+
125
+ return is_body_read_segment(read) ? ISSUE_BODY_PATH.exec(read)?.[0] : undefined
126
+ }
127
+
128
+ function comments_command(command: string): string | undefined {
129
+ if (!is_body_only_issue_read(command)) return undefined
130
+
131
+ const path = rewritable_path(command)
132
+
133
+ if (path === undefined) return undefined
134
+
135
+ return `gh api --paginate ${path}/comments --jq ${COMMENTS_JQ}\n${command}`
136
+ }
137
+
138
+ // The call's input with the comments read prepended, or `undefined` where the comments are already on
139
+ // the tail or the read has no rewritable spelling.
140
+ function comments_input(
141
+ call: GuardedCall,
142
+ tail: () => string,
143
+ ): Record<string, unknown> | undefined {
144
+ if (call.name !== cost_blocks.BASH_TOOL || !json_value.is_record(call.input)) return undefined
145
+
146
+ const command = comments_command(time_shell.bash_command(call.input))
147
+
148
+ if (command === undefined || prior_comment_read.already_read_for_call(tail(), call)) {
149
+ return undefined
150
+ }
151
+
152
+ return { ...call.input, command }
153
+ }
97
154
 
98
155
  // The enumeration row itself, so `delivered-rules.ts` spreads one entry. `already_satisfied` stands it
99
156
  // down once the comments are on the transcript tail, and until then it refuses every body-only read.
@@ -103,13 +160,17 @@ const ROW = {
103
160
  reason: ISSUE_COMMENTS_REASON,
104
161
  already_satisfied: prior_comment_read.already_read_for_call,
105
162
  keeps: bash_triggers.on_bash_command(reads_issue_comments),
163
+ rewrite: comments_input,
164
+ rewrite_note: COMMENTS_NOTE,
106
165
  // The opening the reason carried before it led with the command.
107
166
  former_reasons: ["⛔ an Issue's comments are part of the Issue: read them before implementing"],
108
167
  }
109
168
 
110
169
  const issue_comments = {
170
+ COMMENTS_NOTE,
111
171
  ISSUE_COMMENTS_REASON,
112
172
  ROW,
173
+ comments_input,
113
174
  is_body_only_issue_read,
114
175
  }
115
176
 
@@ -30,10 +30,13 @@ const KEYWORD_BEFORE_ARGUMENT = -2
30
30
 
31
31
  const PROMPT_LINE_SCHEMA = z.object({
32
32
  isMeta: z.boolean().nullish(),
33
+ timestamp: z.string().nullish(),
33
34
  origin: z.object({ kind: z.string().nullish() }).nullish(),
34
35
  message: z.object({ content: z.unknown() }),
35
36
  })
36
37
 
38
+ const TIMED_LINE_SCHEMA = z.object({ timestamp: z.string() })
39
+
37
40
  type PromptLine = z.infer<typeof PROMPT_LINE_SCHEMA>
38
41
 
39
42
  function is_person(line: PromptLine): boolean {
@@ -42,13 +45,20 @@ function is_person(line: PromptLine): boolean {
42
45
  return line.origin === null || line.origin === undefined || line.origin.kind === HUMAN_ORIGIN
43
46
  }
44
47
 
45
- // The prompt line's text, or `undefined` when the harness wrote it or it does not read as one.
46
- function person_text(line: string): string | undefined {
48
+ // The prompt line as the person wrote it, or `undefined` when the harness wrote it or it does not
49
+ // read as one.
50
+ function person_line(line: string): PromptLine | undefined {
47
51
  const parsed = PROMPT_LINE_SCHEMA.safeParse(json_value.parse_or_undefined(line))
48
52
 
49
- if (!parsed.success || !is_person(parsed.data)) return undefined
53
+ return parsed.success && is_person(parsed.data) ? parsed.data : undefined
54
+ }
50
55
 
51
- return time_reported_failure.result_text(parsed.data.message.content)
56
+ // The prompt line's text, or `undefined` when the harness wrote it or it does not read as one.
57
+ function person_text(line: string): string | undefined {
58
+ const person = person_line(line)
59
+ if (person === undefined) return undefined
60
+
61
+ return time_reported_failure.result_text(person.message.content)
52
62
  }
53
63
 
54
64
  // A keyword opening the prompt is what a person types; a keyword before its one trailing argument is a
@@ -75,6 +85,41 @@ function prompt_text(tail: string): string {
75
85
  return text === undefined || is_invocation(text) ? NO_PROMPT : text
76
86
  }
77
87
 
78
- const last_prompt = { prompt_text }
88
+ // **The prompt that opened this turn, when a person typed it** — '' when the harness opened it (a task
89
+ // notification, a subagent's hand-back), so a question asked turns ago does not answer for a stop the
90
+ // run reached on its own since (`stop-decision.ts` → `is_question`).
91
+ function turn_prompt(tail: string): string {
92
+ const opener = tail.split('\n').findLast((line) => filing_cap.is_prompt_line(line))
93
+ if (opener === undefined) return NO_PROMPT
94
+
95
+ return person_text(opener) ?? NO_PROMPT
96
+ }
97
+
98
+ // When the person last prompted on the tail, or `undefined` when no prompt of theirs is on it.
99
+ function prompt_time(tail: string): string | undefined {
100
+ return tail
101
+ .split('\n')
102
+ .filter((line) => filing_cap.is_prompt_line(line))
103
+ .map((line) => person_line(line)?.timestamp ?? undefined)
104
+ .findLast((time) => time !== undefined)
105
+ }
106
+
107
+ // The first dated line on the tail, or `undefined` when none is.
108
+ function tail_time(tail: string): string | undefined {
109
+ return tail
110
+ .split('\n')
111
+ .map((line) => TIMED_LINE_SCHEMA.safeParse(json_value.parse_or_undefined(line)).data?.timestamp)
112
+ .find((time) => time !== undefined)
113
+ }
114
+
115
+ // **When the current pause began, as far as the tail shows** — what a recorded notify is compared
116
+ // against. A person's prompt ends the pause it reported, so the last one opens the next; with none on
117
+ // the tail, only a send dated inside the tail is provably after every prompt, and an older send is
118
+ // announced again (joshuafolkken/kit#3961).
119
+ function pause_start(tail: string): string | undefined {
120
+ return prompt_time(tail) ?? tail_time(tail)
121
+ }
122
+
123
+ const last_prompt = { pause_start, prompt_text, turn_prompt }
79
124
 
80
125
  export { last_prompt }
@@ -12,6 +12,10 @@ import { oracle_firing } from './oracle-firing'
12
12
  const ARGV_OFFSET = 2
13
13
  const INDENT = ' '
14
14
  const FIELD_WIDTH = 10
15
+ // The rule-prose guard sends a rule author here first, so the list closes on the placement order a new
16
+ // rule is weighed against before it becomes a guard or a document line.
17
+ const AUTOMATION_ORDER =
18
+ 'automate first: a command > a hook that runs it > a refusal guard > a document — a lower rung states why the higher cannot hold (`principles.md` → "automate-first"; a guard\'s `why` is `scripts/rules/guard-remedy.ts`)'
15
19
 
16
20
  function pad_field(label: string): string {
17
21
  return label.padEnd(FIELD_WIDTH)
@@ -51,6 +55,8 @@ function print_list(): void {
51
55
  for (const oracle of decision_oracle.DECISION_ORACLES) {
52
56
  console.info(format_oracle(oracle))
53
57
  }
58
+
59
+ console.info(AUTOMATION_ORDER)
54
60
  }
55
61
 
56
62
  function run(argv: ReadonlyArray<string>): number {
@@ -70,6 +76,7 @@ function main(argv: ReadonlyArray<string>): void {
70
76
  }
71
77
 
72
78
  const oracle_list_cli = {
79
+ AUTOMATION_ORDER,
73
80
  print_list,
74
81
  }
75
82
 
@@ -124,6 +124,8 @@ function keeps_verdict_intact(command: string): boolean {
124
124
  // **Only a filter that reads its input to the end qualifies.** One that exits early — `head`, or a
125
125
  // `grep` stopping at its first match — closes the pipe on a check still writing, and under `pipefail` the
126
126
  // check's SIGPIPE turns a passing run into exit 141; `head` also cuts the verdict line josh prints last.
127
+ // `cut`, `sort`, `uniq` and `wc` read to the end as well, and a `grep … | cut -c1-250` narrowing was the
128
+ // commonest masking still refused after #3875 (joshuafolkken/kit#3920).
127
129
  //
128
130
  // **Each masked check is wrapped on its own, and the rest of the line runs as typed.** Most of the
129
131
  // refusals left after #3570 were a check chained beside other work — `vitest …; pnpm josh lint:related
@@ -141,7 +143,7 @@ function keeps_verdict_intact(command: string): boolean {
141
143
  const PIPEFAIL_OPEN = '(set -o pipefail; '
142
144
  const PIPEFAIL_CLOSE = ')'
143
145
  const COMMENT_START = /(?:^|\s)#/u
144
- const FULL_READING_FILTER = /^(?:tail|grep)\b/u
146
+ const FULL_READING_FILTER = /^(?:tail|grep|cut|sort|uniq|wc)\b/u
145
147
  const EARLY_EXIT_GREP =
146
148
  /^grep\b.*\s(?:-[a-zA-Z]*[lLmq]|--(?:quiet|silent|max-count|files-with(?:out)?-match))/u
147
149
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { delivered_rules } from './delivered-rules'
4
+ import { guard_remedy } from './guard-remedy'
4
5
  import { rule_registry, type RegisteredRule } from './rule-registry'
5
6
 
6
7
  // `josh rule:list` — the list of trigger-delivered rules, rendered from the guard rows themselves.
@@ -12,6 +13,7 @@ import { rule_registry, type RegisteredRule } from './rule-registry'
12
13
  const HEADER =
13
14
  '# Trigger-delivered rules — rendered by `pnpm josh rule:list` from `scripts/rules/delivered-rules.ts`'
14
15
  const MISSING = '(no entry in `scripts/rules/rule-registry.ts`)'
16
+ const MISSING_WHY = '(no `why` in `scripts/rules/guard-remedy.ts`)'
15
17
 
16
18
  function item(id: string, rule: RegisteredRule | undefined): string {
17
19
  if (rule === undefined) return `- \`${id}\` ${MISSING}`
@@ -20,6 +22,7 @@ function item(id: string, rule: RegisteredRule | undefined): string {
20
22
  `- **${rule.title}** (\`${id}\`, ${rule.topic})`,
21
23
  ` - fires: \`${rule.entry}\` — ${rule.fires}`,
22
24
  ` - silent: ${rule.quiet}`,
25
+ ` - not automated: ${guard_remedy.why_of(id) ?? MISSING_WHY}`,
23
26
  ].join('\n')
24
27
  }
25
28
 
@@ -46,6 +49,6 @@ function orphans(): ReadonlyArray<string> {
46
49
 
47
50
  if (process.argv[1] === fileURLToPath(import.meta.url)) process.stdout.write(`${render()}\n`)
48
51
 
49
- const rule_list = { MISSING, orphans, render }
52
+ const rule_list = { MISSING, MISSING_WHY, orphans, render }
50
53
 
51
54
  export { rule_list }
@@ -157,6 +157,16 @@ const GUARD_ROWS: ReadonlyArray<RegisteredRule> = [
157
157
  "a dispatched lane child's `AskUserQuestion`; it hands over the park procedure. Every occurrence",
158
158
  quiet: 'not a lane child, or not an interactive tool',
159
159
  },
160
+ {
161
+ id: 'agent-model',
162
+ title: 'Subagent model named',
163
+ topic:
164
+ '`.claude/skills/workflow-commands/delegation.md` → "A `general-purpose` subagent names its model"',
165
+ entry: RULE_GUARD,
166
+ fires:
167
+ 'an `Agent` / `Task` call of the `general-purpose` type with no `model`. Every occurrence',
168
+ quiet: 'a named agent type, or a `model` set',
169
+ },
160
170
  {
161
171
  id: 'lane-carry-conflict',
162
172
  title: 'Lane child carry commands',
@@ -45,14 +45,16 @@ const MEASURED_ROWS: ReadonlyArray<RegisteredRule> = [
45
45
  },
46
46
  ]
47
47
 
48
- // The `Stop` hook's rows; `stop:guard` judges every stop, so each blocks the stop while it holds.
48
+ // The `Stop` hook's rows; `stop:guard` judges every stop. A row with a fixed answer runs it itself and
49
+ // blocks only when that fails; a row that needs judgement blocks the stop while it holds.
49
50
  const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
50
51
  {
51
52
  id: 'stop-notify',
52
53
  title: 'Stop notification',
53
54
  topic: '`CLAUDE.md` → "Mid-workflow stop notification"',
54
55
  entry: STOP_GUARD,
55
- fires: 'a stop with the work tree held and no `confirmation` notify that turn. Blocks the stop',
56
+ fires:
57
+ 'a stop with the work tree held and no `confirmation` notify that turn. Sends the notify itself; blocks only when the send fails',
56
58
  quiet: 'no hold, a `confirmation` notify on the tail, or a pre-gate cut taken',
57
59
  },
58
60
  {
@@ -60,7 +62,8 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
60
62
  title: 'Hold release',
61
63
  topic: '`.claude/skills/workflow-commands/working-tree-hold.md`',
62
64
  entry: STOP_GUARD,
63
- fires: 'a stop on a clean tree with the `run:hold` record still in place. Blocks the stop',
65
+ fires:
66
+ 'a stop on a clean tree with the `run:hold` record still in place. Releases the hold itself; blocks only when that fails',
64
67
  quiet:
65
68
  'a dirty tree (a `halfrun` stop before the commit, a `needs-human-review` stop) or already released',
66
69
  },
@@ -70,7 +73,7 @@ const STOP_ROWS: ReadonlyArray<RegisteredRule> = [
70
73
  topic: '`prompts/collaboration-workflow/issue-citation.md`',
71
74
  entry: STOP_GUARD,
72
75
  fires:
73
- 'a bare `#N` in the prose of the last reply. Blocks the stop until the reply is reissued with the citation fixed',
76
+ 'a bare `#N` in the prose of the last reply. Prints the citation lines itself; blocks only when a number cannot be cited',
74
77
  quiet: 'link form, inside code or a quote, `#N` right after `PR`, or a GitHub-facing artifact',
75
78
  },
76
79
  {
@@ -0,0 +1,114 @@
1
+ import { hook_decision } from '#scripts/josh/hook-decision'
2
+ import { json_value } from '#scripts/lib/json-value'
3
+ import { time_density_hook } from '#scripts/time-runtime/time-density-hook'
4
+ import { time_hook_transcript } from '#scripts/time-runtime/time-hook-transcript'
5
+ import { delivered_rules, type DeliveredRule } from './delivered-rules'
6
+ import { guard_remedy } from './guard-remedy'
7
+
8
+ // The call a `PreToolUse` hook runs in place of a mechanically fixable refusal (joshuafolkken/kit#3920).
9
+ // Each row `guard-remedy.ts` classes as a rewrite gets the call in turn, so one line that both pipes a
10
+ // check and reads a body is made right by both rows. The rewrite is the rule guard's delivery, so the
11
+ // rule guard's switch turns it off too.
12
+
13
+ interface RuleRewrite {
14
+ // The payload carrying the rewritten input, which the guards judge in place of the typed one.
15
+ payload: string
16
+ input: Record<string, unknown>
17
+ // The notes of the rows that rewrote, in row order.
18
+ note: string
19
+ }
20
+
21
+ interface Folded {
22
+ input: Record<string, unknown>
23
+ notes: ReadonlyArray<string>
24
+ }
25
+
26
+ const REWRITING_RULES: ReadonlyArray<DeliveredRule> = delivered_rules.DELIVERED_RULES.filter(
27
+ (rule) => guard_remedy.is_rewrite(rule.id) && rule.rewrite !== undefined,
28
+ )
29
+
30
+ const NOTE_SEPARATOR = '\n\n'
31
+
32
+ // **A tail that cannot be read is an empty one**, as every guard fails open (`hook-decision.ts`): a
33
+ // throw here would end the hook before any guard judged the call.
34
+ function read_payload_tail(raw_payload: string): string {
35
+ try {
36
+ const payload = hook_decision.parse_hook_payload(raw_payload)
37
+
38
+ return payload
39
+ ? time_density_hook.read_tail(
40
+ time_hook_transcript.transcript_of(payload.transcript_path, payload.agent_id),
41
+ )
42
+ : ''
43
+ } catch {
44
+ return ''
45
+ }
46
+ }
47
+
48
+ function tail_reader(raw_payload: string): () => string {
49
+ const cache: { tail?: string } = {}
50
+
51
+ return function tail(): string {
52
+ cache.tail ??= read_payload_tail(raw_payload)
53
+
54
+ return cache.tail
55
+ }
56
+ }
57
+
58
+ function fold(name: string, typed: Record<string, unknown>, tail: () => string): Folded {
59
+ const folded: Folded = { input: typed, notes: [] }
60
+
61
+ for (const rule of REWRITING_RULES) {
62
+ const input = rule.rewrite?.({ name, input: folded.input }, tail)
63
+
64
+ if (input === undefined) {
65
+ continue
66
+ }
67
+
68
+ folded.input = input
69
+ folded.notes = [...folded.notes, rule.rewrite_note ?? '']
70
+ }
71
+
72
+ return folded
73
+ }
74
+
75
+ // The typed call, beside the whole payload its rewrite is spread back into.
76
+ interface TypedPayload {
77
+ fields: Record<string, unknown>
78
+ name: string
79
+ input: Record<string, unknown>
80
+ }
81
+
82
+ function typed_payload(raw_payload: string): TypedPayload | undefined {
83
+ const fields = json_value.parse_or_undefined(raw_payload)
84
+
85
+ if (!json_value.is_record(fields)) return undefined
86
+
87
+ const input = fields['tool_input']
88
+
89
+ return json_value.is_record(input)
90
+ ? { fields, name: String(fields['tool_name']), input }
91
+ : undefined
92
+ }
93
+
94
+ function rewrite(raw_payload: string): RuleRewrite | undefined {
95
+ const typed = delivered_rules.is_enabled() ? typed_payload(raw_payload) : undefined
96
+
97
+ if (typed === undefined) return undefined
98
+
99
+ const { input, notes } = fold(typed.name, typed.input, tail_reader(raw_payload))
100
+
101
+ if (notes.length === 0) return undefined
102
+
103
+ const note = notes.join(NOTE_SEPARATOR)
104
+
105
+ return { payload: JSON.stringify({ ...typed.fields, tool_input: input }), input, note }
106
+ }
107
+
108
+ const rule_rewrite = {
109
+ NOTE_SEPARATOR,
110
+ rewrite,
111
+ }
112
+
113
+ export type { RuleRewrite }
114
+ export { rule_rewrite }
@@ -5,6 +5,7 @@ import {
5
5
  } from '#scripts/time-runtime/time-transcript-line'
6
6
  import { delivered_rules, type MeasuredRule } from './delivered-rules'
7
7
  import { hook_context_line } from './hook-context-line'
8
+ import { rule_rewrite } from './rule-rewrite'
8
9
 
9
10
  // What a rule is worth on the channel that carries it when its delivery has not fired.
10
11
  //
@@ -287,7 +288,7 @@ function rewritten_id(text: string): string | undefined {
287
288
  return found?.id
288
289
  }
289
290
 
290
- function observe_rewrite(states: Map<string, RuleState>, text: string): void {
291
+ function observe_note(states: Map<string, RuleState>, text: string): void {
291
292
  const id = rewritten_id(text)
292
293
  const state = id === undefined ? undefined : states.get(id)
293
294
 
@@ -297,6 +298,11 @@ function observe_rewrite(states: Map<string, RuleState>, text: string): void {
297
298
  state.is_triggered = true
298
299
  }
299
300
 
301
+ // One line can be rewritten by several rows, whose notes `rule-rewrite.ts` joins into one context.
302
+ function observe_rewrite(states: Map<string, RuleState>, text: string): void {
303
+ for (const note of text.split(rule_rewrite.NOTE_SEPARATOR)) observe_note(states, note)
304
+ }
305
+
300
306
  function observe_for_rule(
301
307
  rule: MeasuredRule,
302
308
  state: RuleState,
@@ -26,6 +26,20 @@ function parts_of(command: string): Array<string> {
26
26
  return command.split(SEGMENT_PARTS)
27
27
  }
28
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
+
29
43
  // **Both spellings match, and the expansion is not this file's.** `josh_command_of` expands the alias
30
44
  // in the command it reads, so `pnpm josh ga` arrives as `josh gate` and a caller's canonical set
31
45
  // matches it as it is. Widening as well would be a second copy of one rule, and the caller that
@@ -41,6 +55,7 @@ function is_josh_command(segment: string, names: ReadonlySet<string>): boolean {
41
55
  }
42
56
 
43
57
  const shell_segments = {
58
+ first_of,
44
59
  is_josh_command,
45
60
  parts_of,
46
61
  segments_of,
@@ -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 }