@joshuafolkken/kit 1.1453.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 (224) 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-handoff-report.md +1 -1
  9. package/.claude/skills/workflow-commands/backlogrun-lanes.md +91 -121
  10. package/.claude/skills/workflow-commands/backlogrun-park.md +73 -117
  11. package/.claude/skills/workflow-commands/backlogrun-progress.md +7 -9
  12. package/.claude/skills/workflow-commands/backlogrun-recovery.md +11 -20
  13. package/.claude/skills/workflow-commands/backlogrun-steps.md +43 -52
  14. package/.claude/skills/workflow-commands/chain-rule.md +24 -34
  15. package/.claude/skills/workflow-commands/followup.md +57 -76
  16. package/.claude/skills/workflow-commands/halfrun.md +2 -1
  17. package/.claude/skills/workflow-commands/issue-comments.md +43 -60
  18. package/.claude/skills/workflow-commands/kickoff.md +65 -74
  19. package/.claude/skills/workflow-commands/observation-filing.md +62 -107
  20. package/.claude/skills/workflow-commands/observation-ledger.md +56 -111
  21. package/.claude/skills/workflow-commands/pre-gate-cut.md +89 -138
  22. package/.claude/skills/workflow-commands/prerequisite.md +10 -15
  23. package/.claude/skills/workflow-commands/progress-watcher.md +21 -26
  24. package/.claude/skills/workflow-commands/prrun.md +2 -1
  25. package/.claude/skills/workflow-commands/retrospective.md +31 -50
  26. package/.claude/skills/workflow-commands/split-assessment.md +34 -58
  27. package/.claude/skills/workflow-commands/working-tree-hold.md +54 -71
  28. package/CLAUDE.md +7 -6
  29. package/README.md +2 -9
  30. package/dist/CLAUDE.md +7 -6
  31. package/dist/hooks/batch-guard.js +6 -6
  32. package/dist/hooks/{chunk-Y3GKIKGW.js → chunk-43ZHT22P.js} +31 -15
  33. package/dist/hooks/{chunk-MBJYQPEK.js → chunk-AS74OU3D.js} +24 -8
  34. package/dist/hooks/{chunk-3C4XRYG6.js → chunk-E7OIKX5Z.js} +17 -12
  35. package/dist/hooks/{chunk-Z4XI7CZS.js → chunk-EUMACUK2.js} +18 -12
  36. package/dist/hooks/{chunk-NO4AALP5.js → chunk-H5HVWYXK.js} +4 -4
  37. package/dist/hooks/{chunk-5C7L6FRC.js → chunk-IIGFYXKC.js} +346 -241
  38. package/dist/hooks/{chunk-W5ZKGTSY.js → chunk-J47HU5H5.js} +14 -3
  39. package/dist/hooks/{chunk-OLMO72ON.js → chunk-K4EOONYM.js} +1 -1
  40. package/dist/hooks/{chunk-LBFYEOXJ.js → chunk-K4LMBQC2.js} +4 -4
  41. package/dist/hooks/{chunk-D3NJXN5M.js → chunk-KC5AK2GF.js} +8 -6
  42. package/dist/hooks/chunk-SHGPANFB.js +56 -0
  43. package/dist/hooks/{chunk-EF5GQ6WS.js → chunk-YHBZAJSV.js} +3 -3
  44. package/dist/hooks/{chunk-SHYUQOIS.js → chunk-YYPI7SJZ.js} +5 -5
  45. package/dist/hooks/codex-hook-adapter.js +13 -13
  46. package/dist/hooks/duplicate-read-guard.js +7 -7
  47. package/dist/hooks/format-edited.js +3 -3
  48. package/dist/hooks/inputs.json +251 -247
  49. package/dist/hooks/investigation-guard.js +7 -7
  50. package/dist/hooks/pretool-guard.js +12 -12
  51. package/dist/hooks/session-lang.js +6 -5
  52. package/dist/hooks/stop-guard.js +75 -18
  53. package/dist/josh.js +6 -7
  54. package/dist/managed-marker/index.js +1 -2
  55. package/dist/version/index.js +15 -12
  56. package/eslint/base.js +2 -1
  57. package/eslint/rules/unicorn.js +16 -0
  58. package/package.json +3 -3
  59. package/prompts/coding-standards.md +12 -22
  60. package/prompts/collaboration-workflow/file-edits.md +26 -44
  61. package/prompts/collaboration-workflow/gh-rest.md +20 -36
  62. package/prompts/collaboration-workflow/glossary.md +5 -7
  63. package/prompts/collaboration-workflow/issue-citation.md +7 -13
  64. package/prompts/collaboration-workflow/issue-template.md +29 -29
  65. package/prompts/collaboration-workflow/operating-rules.md +35 -44
  66. package/prompts/collaboration-workflow/output-bounds.md +15 -15
  67. package/prompts/collaboration-workflow/overview.md +14 -19
  68. package/prompts/collaboration-workflow/plan-comment.md +13 -13
  69. package/prompts/collaboration-workflow/principles.md +50 -53
  70. package/prompts/collaboration-workflow/proposal-request.md +1 -9
  71. package/prompts/collaboration-workflow/report-format.md +32 -49
  72. package/prompts/collaboration-workflow/residency.md +10 -10
  73. package/prompts/collaboration-workflow/rule-delivery.md +9 -9
  74. package/prompts/collaboration-workflow/shell-body.md +11 -21
  75. package/prompts/collaboration-workflow/turn-batching.md +8 -14
  76. package/prompts/collaboration-workflow/upstream-interrupt.md +26 -34
  77. package/prompts/collaboration-workflow/wip-cap.md +24 -30
  78. package/prompts/collaboration-workflow.md +24 -25
  79. package/prompts/refactoring.md +33 -43
  80. package/prompts/review-rubric.md +84 -129
  81. package/prompts/review.md +111 -141
  82. package/prompts/sonar-hotspot-handling.md +22 -50
  83. package/prompts/testing-guide.md +48 -99
  84. package/scripts/adopt/adopt-toolkits.ts +2 -4
  85. package/scripts/agent/agent-exit-record.ts +23 -1
  86. package/scripts/agent/codex-agent-argv.ts +7 -2
  87. package/scripts/backlog/backlog-budget-cli.ts +1 -2
  88. package/scripts/backlog/backlog-drive-carried.ts +26 -0
  89. package/scripts/backlog/backlog-drive-cli.ts +10 -5
  90. package/scripts/backlog/backlog-drive-named.ts +2 -4
  91. package/scripts/backlog/backlog-drive-restore.ts +19 -1
  92. package/scripts/backlog/backlog-drive-seats.ts +27 -0
  93. package/scripts/backlog/backlog-drive.ts +11 -6
  94. package/scripts/backlog/backlog-ready.ts +13 -6
  95. package/scripts/behavior/behavior-rules.ts +3 -2
  96. package/scripts/cost-runtime/codex-usage.ts +4 -2
  97. package/scripts/cost-runtime/cost-cli.ts +1 -2
  98. package/scripts/delegation/investigation-guard.ts +1 -1
  99. package/scripts/delegation/investigation-reads.ts +1 -1
  100. package/scripts/discovery/repo-discovery.ts +4 -2
  101. package/scripts/discovery/repo-origin.ts +2 -4
  102. package/scripts/doctor/doctor-logic.ts +6 -3
  103. package/scripts/document/document-byte-budget.ts +27 -27
  104. package/scripts/document/document-byte-check.ts +1 -2
  105. package/scripts/document/entry-read-budget.ts +2 -2
  106. package/scripts/epic/epic-audit-rationale.ts +8 -2
  107. package/scripts/epic/epic-bundle-cli.ts +1 -2
  108. package/scripts/epic/epic-chains.ts +6 -2
  109. package/scripts/epic/epic-close-comment.ts +1 -2
  110. package/scripts/gate/verification-gate.ts +7 -2
  111. package/scripts/gh/git-pr-ai-review.ts +12 -1
  112. package/scripts/gh/git-pr-confirmation.ts +11 -7
  113. package/scripts/gh/git-pr.ts +6 -2
  114. package/scripts/git/git-common-directory.ts +3 -3
  115. package/scripts/git/git-diff-reads.ts +16 -9
  116. package/scripts/git/git-execa-error.ts +1 -2
  117. package/scripts/hooks/format-edited-file.ts +10 -4
  118. package/scripts/hooks/pre-push-unit.ts +15 -4
  119. package/scripts/hooks/pretool-guard.ts +23 -29
  120. package/scripts/hooks/step-zero-notice.ts +3 -2
  121. package/scripts/init/hook-command-rewrite.ts +6 -2
  122. package/scripts/init/init-logic-deploy-vps.ts +4 -2
  123. package/scripts/issue/behavior-change-lint.ts +12 -6
  124. package/scripts/issue/issue-scout-cli.ts +3 -2
  125. package/scripts/josh/file-map-stamp.ts +1 -3
  126. package/scripts/josh/hook-decision.ts +17 -2
  127. package/scripts/josh/josh-commands-workflow.ts +1 -1
  128. package/scripts/lane/lane-await.ts +3 -2
  129. package/scripts/lane/lane-child-invocation.ts +14 -1
  130. package/scripts/lane/lane-guard-policy.ts +4 -4
  131. package/scripts/lane/lane-ledger.ts +9 -0
  132. package/scripts/lane/lane-load.ts +9 -2
  133. package/scripts/lane/lane-measure-cli.ts +2 -0
  134. package/scripts/lane/lane-parked.ts +18 -0
  135. package/scripts/lane/lane-process-load.ts +125 -0
  136. package/scripts/lane/lane-process-stats.ts +93 -0
  137. package/scripts/lane/lane-registry.ts +8 -3
  138. package/scripts/lane/lane-session-broker.ts +29 -5
  139. package/scripts/lane/lane-spent.ts +14 -3
  140. package/scripts/lane/lane-stats.ts +1 -0
  141. package/scripts/managed-marker/managed-marker-logic.ts +1 -2
  142. package/scripts/notify/confirmation-options.ts +59 -0
  143. package/scripts/notify/notify-opt-in.ts +33 -0
  144. package/scripts/notify/telegram-notify.ts +25 -8
  145. package/scripts/notify/telegram-test-logic.ts +33 -1
  146. package/scripts/notify/telegram-test.ts +4 -2
  147. package/scripts/overrides/lockfile-overrides.ts +1 -3
  148. package/scripts/propagate/propagate.ts +1 -2
  149. package/scripts/release/release-progress.ts +3 -2
  150. package/scripts/report/report-lint.ts +7 -4
  151. package/scripts/review/review-record-cli.ts +6 -2
  152. package/scripts/rules/destructive-command.ts +6 -9
  153. package/scripts/rules/direct-filing.ts +3 -7
  154. package/scripts/rules/early-heartbeat.ts +7 -18
  155. package/scripts/rules/file-body.ts +6 -13
  156. package/scripts/rules/filing-cap.ts +3 -6
  157. package/scripts/rules/git-force.ts +5 -9
  158. package/scripts/rules/implementation-cut.ts +37 -35
  159. package/scripts/rules/index-guard.ts +4 -7
  160. package/scripts/rules/issue-comments.ts +5 -15
  161. package/scripts/rules/josh-git-bare.ts +3 -6
  162. package/scripts/rules/lane-background.ts +10 -21
  163. package/scripts/rules/lane-carry-conflict.ts +7 -17
  164. package/scripts/rules/lane-interactive-ask.ts +6 -11
  165. package/scripts/rules/lane-park.ts +5 -13
  166. package/scripts/rules/lane-split-park.ts +2 -7
  167. package/scripts/rules/lane-switch-main.ts +9 -12
  168. package/scripts/rules/oracle-consulted.ts +4 -5
  169. package/scripts/rules/piped-verification.ts +81 -31
  170. package/scripts/rules/poll-loop.ts +7 -17
  171. package/scripts/rules/pre-gate-cut.ts +7 -21
  172. package/scripts/rules/prior-comment-read.ts +7 -5
  173. package/scripts/rules/protected-files.ts +5 -9
  174. package/scripts/rules/raw-field-body.ts +3 -7
  175. package/scripts/rules/rule-body-guard.ts +4 -11
  176. package/scripts/rules/run-tail-rule.ts +6 -22
  177. package/scripts/rules/shell-body-trigger.ts +7 -8
  178. package/scripts/rules/shell-segments.ts +7 -0
  179. package/scripts/rules/stop-rules.ts +17 -29
  180. package/scripts/rules/test-declared-commit.ts +4 -10
  181. package/scripts/rules/third-party-write.ts +5 -10
  182. package/scripts/rules/worktree-guard.ts +5 -12
  183. package/scripts/run/board/run-board-github.ts +11 -3
  184. package/scripts/run/board/run-board-idle.ts +31 -0
  185. package/scripts/run/board/run-board-read.ts +9 -12
  186. package/scripts/run/board/run-board-reason.ts +7 -3
  187. package/scripts/run/board/run-board-state.ts +4 -0
  188. package/scripts/run/board/run-board-status.ts +18 -3
  189. package/scripts/run/board/run-board-thanks.ts +7 -2
  190. package/scripts/run/board/run-board-tick.ts +32 -7
  191. package/scripts/run/carry/run-carry-args.ts +1 -3
  192. package/scripts/run/carry/run-carry-conversation.ts +7 -2
  193. package/scripts/run/cut/run-cut-cli.ts +15 -20
  194. package/scripts/run/cut/run-cut-decline.ts +72 -0
  195. package/scripts/run/cut/run-cut-ready.ts +74 -0
  196. package/scripts/run/cut/run-cut-report.ts +13 -0
  197. package/scripts/run/cut/run-cut.ts +10 -1
  198. package/scripts/run/entry/run-entry-stop.ts +6 -3
  199. package/scripts/run/hold/run-hold-cli.ts +14 -3
  200. package/scripts/run/hold/run-hold.ts +6 -3
  201. package/scripts/run/merge/run-merge-cli.ts +30 -7
  202. package/scripts/run/merge/run-merge-ship.ts +59 -1
  203. package/scripts/run/merge/run-merge.ts +3 -2
  204. package/scripts/run/run-ending.ts +31 -3
  205. package/scripts/run/run-parent-cut-hook.ts +5 -10
  206. package/scripts/run/run-preflight.ts +1 -2
  207. package/scripts/run/run-stop-notify.ts +3 -2
  208. package/scripts/run/run-watcher-guard.ts +6 -3
  209. package/scripts/run/ship/run-ship-review-steps.ts +13 -4
  210. package/scripts/run/ship/run-ship-review.ts +15 -2
  211. package/scripts/run/tidy/run-tidy-cli.ts +3 -3
  212. package/scripts/run/tidy/run-tidy-lanes.ts +3 -2
  213. package/scripts/safe-chain/project-config.ts +7 -2
  214. package/scripts/sync/sync-hook-safety.ts +8 -2
  215. package/scripts/test/unit-worker-share.ts +3 -2
  216. package/scripts/time-runtime/time-batch-guard.ts +3 -3
  217. package/scripts/time-runtime/time-hook-transcript.ts +3 -2
  218. package/scripts/time-runtime/time-shell.ts +10 -0
  219. package/scripts/version/effective-upstream.ts +3 -4
  220. package/scripts/version/kit-version-config.ts +8 -2
  221. package/scripts/version/latest-corepack.ts +1 -2
  222. package/scripts/version/release-hold.ts +11 -4
  223. package/scripts/version/version-commands.ts +12 -4
  224. package/dist/hooks/chunk-UT7YC3N2.js +0 -56
@@ -178,10 +178,13 @@ function issues_traced(traced: ReadonlyArray<TracedLane>, trace: ProcessTrace):
178
178
  return traced.filter(([, answer]) => answer === trace).map(([issue]) => issue)
179
179
  }
180
180
 
181
+ function traced_of(lanes: ReadonlyArray<LaneInfo>, probes: SpentProbes): Array<TracedLane> {
182
+ return lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
183
+ }
184
+
181
185
  /** The lanes in use — what a run being replaced must not be stopped over. */
182
186
  async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse> {
183
- const lanes = await lane_registry.list_lanes()
184
- const traced = lanes.map((lane): TracedLane => [lane.issue, trace_of(lane, probes)])
187
+ const traced = traced_of(await lane_registry.list_lanes(), probes)
185
188
 
186
189
  return {
187
190
  working: issues_traced(traced, PROCESS_ALIVE),
@@ -189,7 +192,15 @@ async function lanes_in_use(probes: SpentProbes = LIVE_PROBES): Promise<LaneUse>
189
192
  }
190
193
  }
191
194
 
192
- const lane_spent = { kept_reason, lanes_in_use, sort_lanes }
195
+ /** The lanes no child is working in, read as such — never one whose probe did not answer. */
196
+ function idle_lanes(
197
+ lanes: ReadonlyArray<LaneInfo>,
198
+ probes: SpentProbes = LIVE_PROBES,
199
+ ): Array<string> {
200
+ return issues_traced(traced_of(lanes, probes), PROCESS_NONE)
201
+ }
202
+
203
+ const lane_spent = { idle_lanes, kept_reason, lanes_in_use, sort_lanes }
193
204
 
194
205
  export type { KeptLane, LaneUse, SpentProbes, SpentSort }
195
206
  export { lane_spent }
@@ -258,6 +258,7 @@ const lane_stats = {
258
258
  MISSING,
259
259
  header,
260
260
  in_window,
261
+ mean,
261
262
  median,
262
263
  peak,
263
264
  row,
@@ -46,8 +46,7 @@ function apply_marker_for_destination(
46
46
  text: string,
47
47
  package_name: string,
48
48
  ): string {
49
- if (!is_workflow_destination(destination)) return text
50
- if (is_marked(text)) return text
49
+ if (!is_workflow_destination(destination) || is_marked(text)) return text
51
50
 
52
51
  return `${marker_block(package_name)}${text}`
53
52
  }
@@ -0,0 +1,59 @@
1
+ import { session_language } from '#scripts/josh/session-language'
2
+
3
+ // **A confirmation names what to choose, not only why the run stopped.** The person reading it is
4
+ // off-screen by definition, so the message itself carries the recommended choice and the others —
5
+ // in the session language, because that is the language the person works in.
6
+
7
+ interface ConfirmationOptions {
8
+ recommended: string
9
+ alternatives: ReadonlyArray<string>
10
+ }
11
+
12
+ interface Headings {
13
+ recommended: string
14
+ alternatives: string
15
+ none: string
16
+ }
17
+
18
+ const JAPANESE_LANG_PREFIX = 'ja'
19
+ const JAPANESE_HEADINGS: Headings = {
20
+ recommended: '推奨:',
21
+ alternatives: '他の選択肢:',
22
+ none: 'なし',
23
+ }
24
+ const ENGLISH_HEADINGS: Headings = {
25
+ recommended: 'Recommended:',
26
+ alternatives: 'Alternatives:',
27
+ none: 'none',
28
+ }
29
+
30
+ // Only the two languages the workflow is written in have headings; any other falls back to English.
31
+ function headings(): Headings {
32
+ const { lang } = session_language.resolve_session_lang()
33
+
34
+ return lang.toLowerCase().startsWith(JAPANESE_LANG_PREFIX) ? JAPANESE_HEADINGS : ENGLISH_HEADINGS
35
+ }
36
+
37
+ // An empty list is stated as none, so a confirmation with no other option says so rather than
38
+ // leaving a reader to wonder whether the line was lost.
39
+ function alternatives_block(options: ConfirmationOptions, labels: Headings): string {
40
+ if (options.alternatives.length === 0) return `${labels.alternatives} ${labels.none}`
41
+
42
+ const lines = options.alternatives.map((alternative) => `- ${alternative}`)
43
+
44
+ return [labels.alternatives, ...lines].join('\n')
45
+ }
46
+
47
+ // The block is handed to the sender as its own `choices` field rather than appended to the body:
48
+ // the body is what a too-long message is cut from, and the choices are the one part a reader cannot
49
+ // act without, so they must survive the cut.
50
+ function block(options: ConfirmationOptions): string {
51
+ const labels = headings()
52
+
53
+ return `${labels.recommended} ${options.recommended}\n${alternatives_block(options, labels)}`
54
+ }
55
+
56
+ const confirmation_options = { block }
57
+
58
+ export { confirmation_options }
59
+ export type { ConfirmationOptions }
@@ -0,0 +1,33 @@
1
+ import type { TelegramTaskType } from './telegram-notify'
2
+
3
+ // **`planning` and `completion` reach a person only when they asked for them.** Neither needs the
4
+ // person to act now — a plan is posted on the Issue and a merge is visible on the pull request — so
5
+ // sending both on every run buried the types that do need someone. Every other type is unaffected.
6
+ //
7
+ // **One variable listing the opted-in types, not one switch per type**, so making a further type
8
+ // optional later adds an entry to `OPTIONAL_TASK_TYPES` rather than another variable.
9
+ const OPT_IN_KEY = 'JOSH_NOTIFY_OPT_IN'
10
+ const OPTIONAL_TASK_TYPES: ReadonlySet<TelegramTaskType> = new Set(['planning', 'completion'])
11
+ const LIST_SEPARATOR = ','
12
+
13
+ function opted_in_types(): ReadonlyArray<string> {
14
+ return (process.env[OPT_IN_KEY] ?? '')
15
+ .split(LIST_SEPARATOR)
16
+ .map((entry) => entry.trim().toLowerCase())
17
+ .filter((entry) => entry.length > 0)
18
+ }
19
+
20
+ // Unset means none: an optional type is sent only once it is named.
21
+ function is_skipped(task_type: TelegramTaskType): boolean {
22
+ if (!OPTIONAL_TASK_TYPES.has(task_type)) return false
23
+
24
+ return !opted_in_types().includes(task_type)
25
+ }
26
+
27
+ function skip_line(task_type: TelegramTaskType): string {
28
+ return `🔕 ${task_type} notifications are not opted in (${OPT_IN_KEY}).`
29
+ }
30
+
31
+ const notify_opt_in = { is_skipped, skip_line }
32
+
33
+ export { notify_opt_in }
@@ -1,6 +1,8 @@
1
1
  import { gh_spawn } from '#scripts/gh/gh-spawn'
2
2
  import { timed_fetch } from '#scripts/lib/timed-fetch'
3
3
  import { z } from 'zod'
4
+ import { confirmation_options, type ConfirmationOptions } from './confirmation-options'
5
+ import { notify_opt_in } from './notify-opt-in'
4
6
 
5
7
  const TELEGRAM_API_BASE = 'https://api.telegram.org'
6
8
  // `sendMessage` refuses a text longer than this.
@@ -42,6 +44,9 @@ interface TelegramSendInput {
42
44
  repo_name: string | undefined
43
45
  issue_title: string | undefined
44
46
  body: string | undefined
47
+ // A confirmation's recommended choice and the others — set beside the body rather than inside it
48
+ // so a body cut to the length limit never takes the choices with it.
49
+ choices?: string | undefined
45
50
  issue_url: string | undefined
46
51
  pr_url: string | undefined
47
52
  }
@@ -149,6 +154,16 @@ function is_notify_disabled(): boolean {
149
154
  return parse_environment_string(process.env[NOTIFY_SWITCH_KEY]).toLowerCase() === NOTIFY_OFF_VALUE
150
155
  }
151
156
 
157
+ // The line a skipped send prints, or `undefined` for a send that goes out. The blanket opt-out wins
158
+ // over the per-type opt-in, so `off` stays the one switch that silences everything.
159
+ function skip_line(task_type: TelegramTaskType): string | undefined {
160
+ if (is_notify_disabled()) {
161
+ return `🔕 Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`
162
+ }
163
+
164
+ return notify_opt_in.is_skipped(task_type) ? notify_opt_in.skip_line(task_type) : undefined
165
+ }
166
+
152
167
  function build_header(task_type: TelegramTaskType, repo_name: string | undefined): string {
153
168
  const { icon, label } = TASK_DEFINITIONS[task_type]
154
169
  if (repo_name === undefined || repo_name.length === 0) return `${icon} ${label}`
@@ -185,6 +200,7 @@ function build_blocks(input: TelegramSendInput): Array<string> {
185
200
  const blocks: Array<string> = [build_title_block(input)]
186
201
 
187
202
  push_if_present(blocks, input.body)
203
+ push_if_present(blocks, input.choices)
188
204
  blocks.push(...build_url_parts(input))
189
205
 
190
206
  return blocks
@@ -243,13 +259,13 @@ function build_send_failure(error: unknown, config: TelegramConfig): Error {
243
259
  //
244
260
  // The failure it throws carries a `cause`, and that cause is redacted too — see `build_send_failure`.
245
261
  //
246
- // The opt-out is checked here, before `load_config`, so every caller — the tolerant form included —
247
- // skips the same way and none has to repeat the check.
262
+ // The opt-out and the per-type opt-in are checked here, before `load_config`, so every caller — the
263
+ // tolerant form included — skips the same way and none has to repeat the check.
248
264
  async function send(input: TelegramSendInput): Promise<void> {
249
- if (is_notify_disabled()) {
250
- console.info(
251
- `🔕 Telegram notifications are disabled (${NOTIFY_SWITCH_KEY}=${NOTIFY_OFF_VALUE}).`,
252
- )
265
+ const skipped = skip_line(input.task_type)
266
+
267
+ if (skipped !== undefined) {
268
+ console.info(skipped)
253
269
 
254
270
  return
255
271
  }
@@ -341,7 +357,7 @@ async function warn(input: WarningInput): Promise<boolean> {
341
357
  )
342
358
  }
343
359
 
344
- interface ConfirmInput {
360
+ interface ConfirmInput extends ConfirmationOptions {
345
361
  issue_title: string
346
362
  body: string
347
363
  recovery: string
@@ -365,6 +381,7 @@ async function confirm(input: ConfirmInput): Promise<boolean> {
365
381
  repo_name: gh_spawn.get_repo_name_with_owner_within(REPO_LOOKUP_TIMEOUT_MS),
366
382
  issue_title: input.issue_title,
367
383
  body: input.body,
384
+ choices: confirmation_options.block(input),
368
385
  issue_url: undefined,
369
386
  pr_url: undefined,
370
387
  },
@@ -463,4 +480,4 @@ const telegram_notify = {
463
480
  }
464
481
 
465
482
  export { telegram_notify, build_text, TASK_DEFINITIONS, telegram_environment_schema }
466
- export type { TelegramSendInput, TelegramTaskType }
483
+ export type { ConfirmInput, TelegramSendInput, TelegramTaskType }
@@ -1,4 +1,5 @@
1
1
  import { cli_body } from '#scripts/josh/cli-body'
2
+ import { confirmation_options } from './confirmation-options'
2
3
  import type { TelegramSendInput, TelegramTaskType } from './telegram-notify'
3
4
 
4
5
  /* eslint-disable @typescript-eslint/naming-convention */
@@ -10,6 +11,8 @@ interface CliValues {
10
11
  'body-file'?: string
11
12
  'issue-url'?: string
12
13
  'pr-url'?: string
14
+ recommended?: string
15
+ alternative?: Array<string>
13
16
  }
14
17
  /* eslint-enable @typescript-eslint/naming-convention */
15
18
 
@@ -67,12 +70,41 @@ function normalize_body(values: CliValues): string | undefined {
67
70
  })
68
71
  }
69
72
 
73
+ // **A confirmation without a recommended choice is refused, not sent**: the person it reaches is
74
+ // off-screen, and a stop that names no choice leaves them to reconstruct one. `--alternative` may be
75
+ // absent — a stop with one way forward says so — so only `--recommended` is required.
76
+ function require_recommended(values: CliValues): string {
77
+ const recommended = values.recommended?.trim() ?? ''
78
+
79
+ if (recommended.length === 0) {
80
+ throw new Error('--task-type confirmation needs --recommended "<the recommended choice>".')
81
+ }
82
+
83
+ return recommended
84
+ }
85
+
86
+ function given_alternatives(values: CliValues): Array<string> {
87
+ return (values.alternative ?? []).filter((alternative) => alternative.trim().length > 0)
88
+ }
89
+
90
+ function build_choices(values: CliValues, task_type: TelegramTaskType): string | undefined {
91
+ if (task_type !== 'confirmation') return undefined
92
+
93
+ return confirmation_options.block({
94
+ recommended: require_recommended(values),
95
+ alternatives: given_alternatives(values),
96
+ })
97
+ }
98
+
70
99
  function build_input(input: { values: CliValues; context: ResolvedContext }): TelegramSendInput {
100
+ const task_type = parse_task_type(input.values['task-type'])
101
+
71
102
  return {
72
- task_type: parse_task_type(input.values['task-type']),
103
+ task_type,
73
104
  repo_name: coalesce(input.values['repo-name'], input.context.repo_name),
74
105
  issue_title: coalesce(input.values['issue-title'], input.context.issue_title),
75
106
  body: normalize_body(input.values),
107
+ choices: build_choices(input.values, task_type),
76
108
  issue_url: input.values['issue-url'],
77
109
  pr_url: input.values['pr-url'],
78
110
  }
@@ -14,10 +14,10 @@ const REPO_NAME_SEPARATOR = '/'
14
14
  const ARGV_OFFSET = 2
15
15
  const FAILURE_EXIT_CODE = 1
16
16
  const USAGE =
17
- 'Usage: josh notify [--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>]'
17
+ 'Usage: josh notify [--task-type <type>] [--body <text> | --body-file <path>] [--issue-url <url>] [--pr-url <url>] [--issue-title <text>] [--repo-name <name>] [--recommended <text>] [--alternative <text>]...'
18
18
 
19
19
  // The free-text values a caller writes, often opening with a Markdown `-` bullet.
20
- const FREE_TEXT_FLAGS = ['--body', '--issue-title']
20
+ const FREE_TEXT_FLAGS = ['--body', '--issue-title', '--recommended', '--alternative']
21
21
 
22
22
  const OPTIONS = {
23
23
  'task-type': { type: 'string' },
@@ -27,6 +27,8 @@ const OPTIONS = {
27
27
  'body-file': { type: 'string' },
28
28
  'issue-url': { type: 'string' },
29
29
  'pr-url': { type: 'string' },
30
+ recommended: { type: 'string' },
31
+ alternative: { type: 'string', multiple: true },
30
32
  } as const
31
33
 
32
34
  // `undefined` for a line nobody can read — an unknown flag, or a flag given no value.
@@ -42,9 +42,7 @@ function find_entry_mismatch(
42
42
  specifier: string,
43
43
  override: string | undefined,
44
44
  ): Array<SpecifierMismatch> {
45
- if (override === undefined) return []
46
- if (is_non_registry(specifier)) return []
47
- if (specifier === override) return []
45
+ if (override === undefined || specifier === override || is_non_registry(specifier)) return []
48
46
 
49
47
  return [{ importer, name, lockfile_specifier: specifier, override }]
50
48
  }
@@ -277,8 +277,7 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
277
277
  }
278
278
 
279
279
  const plan = plan_run(options)
280
- if (plan === undefined) return FAILURE_EXIT_CODE
281
- if (!(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
280
+ if (plan === undefined || !(await resolve_publish(plan, options))) return FAILURE_EXIT_CODE
282
281
 
283
282
  return propagate_to_consumers(plan, options.is_dry_run)
284
283
  }
@@ -145,8 +145,9 @@ async function wait_for_github_release(version: string): Promise<boolean> {
145
145
  async function wait_for_distribution(version: string): Promise<number> {
146
146
  const manifest = read_manifest(process.cwd())
147
147
 
148
- if (!(await wait_for_npm(manifest, version))) return FAILURE_EXIT_CODE
149
- if (!(await wait_for_github_release(version))) return FAILURE_EXIT_CODE
148
+ if (!(await wait_for_npm(manifest, version)) || !(await wait_for_github_release(version))) {
149
+ return FAILURE_EXIT_CODE
150
+ }
150
151
 
151
152
  console.info(complete_line(version))
152
153
 
@@ -102,8 +102,7 @@ function overview_lines(summary: string): ReadonlyArray<string> {
102
102
  function item_sentence(line: string): string {
103
103
  const open = line.indexOf(BOLD_MARKER)
104
104
  const close = open === -1 ? -1 : line.indexOf(BOLD_MARKER, open + BOLD_MARKER.length)
105
- const raw =
106
- close === -1 ? line.slice(ITEM_LINE_PREFIX.length) : line.slice(close + BOLD_MARKER.length)
105
+ const raw = line.slice(close === -1 ? ITEM_LINE_PREFIX.length : close + BOLD_MARKER.length)
107
106
 
108
107
  return raw.replace(LEADING_LABEL_PUNCTUATION, '').trim()
109
108
  }
@@ -199,8 +198,12 @@ function break_condition_count(lines: ReadonlyArray<string>): number {
199
198
  }
200
199
 
201
200
  function case_line_violations(summary: string): ReadonlyArray<string> {
202
- if (!has_changes_section(summary)) return []
203
- if (changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))) return []
201
+ if (
202
+ !has_changes_section(summary) ||
203
+ changes_region_lines(summary).some((line) => line.includes(CASE_LABEL))
204
+ ) {
205
+ return []
206
+ }
204
207
 
205
208
  return [`✖ no case tier (${CASE_LABEL}:) in ${CHANGES_LABEL}`]
206
209
  }
@@ -58,8 +58,12 @@ interface Parsed {
58
58
  // A finding spec is `<category>:<severity>:<file>`; the file field keeps any `:line` a citation
59
59
  // carries, so only the first two colons separate fields.
60
60
  function to_finding(category: string, severity: string, file: string): Finding | undefined {
61
- if (!review_finding_ledger.is_category(category)) return undefined
62
- if (!review_finding_ledger.is_severity(severity)) return undefined
61
+ if (
62
+ !review_finding_ledger.is_category(category) ||
63
+ !review_finding_ledger.is_severity(severity)
64
+ ) {
65
+ return undefined
66
+ }
63
67
 
64
68
  return review_finding_ledger.is_valid_file(file) ? { category, severity, file } : undefined
65
69
  }
@@ -97,21 +97,18 @@ function is_destructive_command(command: string): boolean {
97
97
  }
98
98
 
99
99
  const DESTRUCTIVE_COMMAND_REASON =
100
- '⛔ destructive command: a recursive forced `rm` (`rm -rf`, `rm -fr`, `rm -r -f`, `rm --recursive ' +
101
- '--force`), `gh repo delete` / `archive`, `gh repo edit --visibility`, `gh pr close`, or a `gh api ' +
102
- '-X DELETE repos/…` call deletes or closes something that cannot be restored, which is Tier C and ' +
103
- 'needs explicit current-turn user instruction (`prompts/collaboration-workflow/operating-rules.md`). ' +
104
- 'The `.claude/settings.json` deny list matches prefix globs, so it misses these spellings; this row ' +
105
- 'reads the argv instead. Remove a file or directory one path at a time without `-f`, or ask the user ' +
106
- 'to run the command in their own terminal. `gh issue close` and removing a label from an Issue ' +
107
- '(`DELETE repos/<o>/<r>/issues/<N>/labels/<name>`) are not refused. **This rule fires on every ' +
108
- 'occurrence, not once per run.**'
100
+ '⛔ destructive command: a recursive forced `rm`, a `gh repo delete` / `archive` / visibility edit, ' +
101
+ '`gh pr close` or a `gh api -X DELETE` cannot be undone, which is Tier C. Remove one path at a ' +
102
+ 'time without `-f`, or ask the user to run it. It fires on every occurrence ' +
103
+ '(`prompts/collaboration-workflow/operating-rules.md`).'
109
104
 
110
105
  const ROW = {
111
106
  id: 'destructive-command',
112
107
  is_trigger: bash_triggers.on_bash_command(is_destructive_command),
113
108
  reason: DESTRUCTIVE_COMMAND_REASON,
114
109
  decide: (): boolean => true,
110
+ // The opening the reason carried before it was cut to the action and the next command.
111
+ former_reasons: ['⛔ destructive command: a recursive forced `rm` (`rm -rf`'],
115
112
  }
116
113
 
117
114
  const destructive_command = { DESTRUCTIVE_COMMAND_REASON, ROW, is_destructive_command }
@@ -9,13 +9,9 @@ import { bash_triggers } from './bash-triggers'
9
9
 
10
10
  const DIRECT_FILING_REASON =
11
11
  '⛔ direct filing: an Issue is filed with `pnpm josh issue:file "<title>" --body-file <path> ' +
12
- '--depth <0|1|2> [--route <route>] [--label <name>] [--repo <owner/repo>]`, never with `gh api …/issues` ' +
13
- 'or `gh issue create`. The command runs every filing step in order — the duplicate scout, the body ' +
14
- 'lint and its classification labels, the depth and `route:` labels, the `## Origin` check when filing ' +
15
- 'into another repository, and `epic:bundle` after the create — so none of them is a step to remember. ' +
16
- 'When the scout prints candidates it holds the filing: read each, then reissue with `--distinct <N,…>` ' +
17
- 'for the ones that are separate deliverables. Reference: `docs/josh-commands-backlog.md` → "`josh issue:file`". ' +
18
- '**This rule fires on every occurrence, not once per run.**'
12
+ '--depth <0|1|2>`, never with `gh api …/issues` or `gh issue create` — it runs the scout, the lint, ' +
13
+ 'the labels and `epic:bundle` itself. It fires on every occurrence (`docs/josh-commands-backlog.md` ' +
14
+ '→ "`josh issue:file`").'
19
15
 
20
16
  function is_any_filing(command: string): boolean {
21
17
  return bash_triggers.is_direct_filing(command) || bash_triggers.is_issue_filing(command)
@@ -178,25 +178,14 @@ function decide(call: GuardedCall, run: GuardRun, can_record: boolean): boolean
178
178
  }
179
179
 
180
180
  // The instruction in the shape a refusal can carry: what the call is about to do, what already does it
181
- // properly, and the one command an explicit ask is answered with. The observed failure is named
182
- // because it is what makes the rule believable.
181
+ // properly, and the one command an explicit ask is answered with. The observed failure behind it is in
182
+ // `docs/maintainers/rule-delivery-rationale.md`.
183
183
  const EARLY_HEARTBEAT_REASON =
184
- '⛔ early heartbeat: this call arms a wait timer of its own, and the progress clock is not yours ' +
185
- 'to keep. `pnpm josh run:progress --wait` keeps it for you ' +
186
- '(`JOSH_PROGRESS_INTERVAL_MINUTES`, or `josh.progress_interval_minutes` in `package.json`, ' +
187
- 'default 20 minutes): start it once in the background and it reports the five labelled lines to ' +
188
- "the run's event stream on its own, without waking you — relay nothing, and start it again only " +
189
- 'after it exits, which it does when newly runnable work arrives. Two timers armed at ' +
190
- 'once is what produced reports 3–5 minutes apart on a 15-minute setting ' +
191
- '(joshuafolkken/kit#1570): a new one was armed on the turn a timer fired and again on the turn a ' +
192
- "child's completion woke the run, and `--mark` recorded each report without anything refusing the " +
193
- 'early ones. **An explicit ask is not a heartbeat**: if the person asked for progress now, run ' +
194
- '`pnpm josh run:progress --once`, which prints the five labelled lines whatever the clock says — ' +
195
- 'under `backlogrun`, `pnpm josh run:board --chat` instead. If you are ' +
196
- 'waiting on something rather than on a clock, wait on the thing itself — `pnpm josh followup` ' +
197
- 'waits for CI. The procedure is `.claude/skills/workflow-commands/backlogrun.md` → "Progress while ' +
198
- 'the run is quiet". **This rule fires on every early arm, not once per run**, so reissuing the ' +
199
- 'same sleep will be refused again.'
184
+ '⛔ early heartbeat: this call arms a wait timer of its own. Start `pnpm josh run:progress --wait` ' +
185
+ "once in the background: it reports to the run's event stream on its own — relay nothing. When " +
186
+ 'asked, `pnpm josh run:progress --once` — under `backlogrun`, `pnpm josh run:board --chat`; ' +
187
+ 'for CI, `pnpm josh followup`. It fires on every occurrence ' +
188
+ '(`.claude/skills/workflow-commands/progress-watcher.md`).'
200
189
 
201
190
  // **Keeping this rule is letting the watcher hold the clock** — `pnpm josh run:progress --wait` waits
202
191
  // the interval out, and `--once` answers an explicit ask. `--mark` is neither: it records a report
@@ -171,20 +171,13 @@ function carries_a_file_body(
171
171
  )
172
172
  }
173
173
 
174
- // The instruction in the shape a refusal can carry: what the command is doing, the safe tool, and the
175
- // file forms that stay allowed. The measured cost is named because it is the half that reads as
176
- // surprising — the command body is billed on every later request of the run.
174
+ // The instruction in the shape a refusal can carry: what the command is doing and the safe tool. The
175
+ // measured cost behind it is in `file-edits.md`.
177
176
  const FILE_BODY_REASON =
178
- "⛔ file body in a shell command: this carries a file's new text inline — a heredoc rewriting an " +
179
- 'existing file, an interpreter (`python3 - <<`, `node -e`) whose body writes, a script file (`cat > ' +
180
- 'edit.py <<EOF`) whose body writes, or an in-place `perl -0pi -e`. `CLAUDE.md` forbids it because ' +
181
- 'the command body is re-read on every later request of the run, so an editing script written early ' +
182
- 'is billed for the rest of it (`file-edits.md` measured one run at ~2m45s spent retyping existing ' +
183
- 'lines). Edit a region with the Edit tool (old / new); a change scattered so widely that the ' +
184
- 'partial edits would carry more than the file is rewritten whole with the Write tool, never a ' +
185
- 'heredoc. Pass a body by path (`--field body=@<path>`, `--body-file <path>`). A new-file creation (`cat > ' +
186
- '<new-file> <<EOF`), a read-only heredoc (`cat <<EOF`), and a short `sed -i` stay allowed. **This ' +
187
- 'rule fires on every occurrence, not once per run.**'
177
+ "⛔ file body in a shell command: this carries a file's new text inline, which is re-read on every " +
178
+ 'later request. Edit a region with the Edit tool, rewrite a whole file with the Write tool, and ' +
179
+ 'pass a body by path (`--body-file <path>`); a new-file `cat > <new-file> <<EOF` stays allowed. ' +
180
+ 'This rule fires on every occurrence (`prompts/collaboration-workflow/file-edits.md`).'
188
181
 
189
182
  const ROW = {
190
183
  id: 'file-body',
@@ -104,12 +104,9 @@ function decide(_call: unknown, run: GuardRun): boolean {
104
104
  }
105
105
 
106
106
  const FILING_CAP_REASON =
107
- `⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling — on ` +
108
- 'reaching it, stop and report rather than filing more. Automatic filing is capped so a run cannot ' +
109
- 'manufacture Issues faster than they can be closed (`.claude/skills/workflow-commands/SKILL.md` → ' +
110
- "`prerequisite.md`, `observation-filing.md`). A further Issue that genuinely must exist is a person's call, not " +
111
- "this run's. **This rule fires on every filing past the cap, not once per run**, so reissuing the " +
112
- 'same filing will be refused again.'
107
+ `⛔ filing cap: this run has already filed ${String(FILING_CAP)} Issues, the per-run ceiling. Stop ` +
108
+ "filing and report; a further Issue is a person's call. It fires on every occurrence " +
109
+ '(`.claude/skills/workflow-commands/observation-filing.md`).'
113
110
 
114
111
  // The enumeration row itself, so `delivered-rules.ts` spreads one entry rather than restating the
115
112
  // trigger and reason it already single-sources here. It declares no `keeps`: staying under a cap is
@@ -69,19 +69,15 @@ function is_force_or_delete(command: string): boolean {
69
69
  return shell_segments.segments_of(command).some((segment) => is_destructive_segment(segment))
70
70
  }
71
71
 
72
- // The instruction in the shape a refusal can carry: what the command does, why the deny list did not
73
- // stop it, and the one route left. There is no "safe reissue" the way a backgrounded push is for
72
+ // The instruction in the shape a refusal can carry: what the command does and the one route left. The
73
+ // spellings the deny glob misses are pinned on `is_force_or_delete`, not repeated in the text. There is no "safe reissue" the way a backgrounded push is for
74
74
  // `run-tail` — a force push and a branch delete are Tier C, so the honest next move is a person's, in
75
75
  // their own terminal.
76
76
  const GIT_FORCE_REASON =
77
77
  '⛔ force push or branch delete: this rewrites or removes shared history, which is Tier C and needs ' +
78
- 'explicit current-turn user instruction — never an AI decision, even to fix a problem you caused ' +
79
- '(`prompts/collaboration-workflow/operating-rules.md`). The `.claude/settings.json` deny list ' +
80
- 'matches globs, so it misses this spelling: a combined short cluster (`git push -uf`), a `git -C ' +
81
- '<path>` prefix, and the colon-form delete (`git push origin :branch`) all pass it. This row reads ' +
82
- 'the argv instead, so the spelling does not matter. For an ordinary push use `pnpm josh git`; a ' +
83
- "force push, a `git push --delete` / `:branch`, or a `git branch -d` / `-D` is the user's to run in " +
84
- 'their own terminal. **This rule fires on every occurrence, not once per run.**'
78
+ 'explicit current-turn user instruction. Push with `pnpm josh git`; leave a force push or branch ' +
79
+ "delete to the user's own terminal. This rule fires on every occurrence " +
80
+ '(`prompts/collaboration-workflow/operating-rules.md`).'
85
81
 
86
82
  // The row itself, so `delivered-rules.ts` spreads one entry. `decide` returns true so it refuses every
87
83
  // occurrence; it declares no `keeps` — not force-pushing is the absence of a call, not a call, so the