@joshuafolkken/kit 1.1387.0 → 1.1435.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 (236) hide show
  1. package/.claude/skills/epic-commands/SKILL.md +1 -1
  2. package/.claude/skills/epic-commands/epic-bundle.md +5 -0
  3. package/.claude/skills/workflow-commands/backlogrun-lanes.md +5 -3
  4. package/.claude/skills/workflow-commands/backlogrun-steps.md +4 -3
  5. package/.claude/skills/workflow-commands/backlogrun.md +1 -0
  6. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  7. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  8. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  9. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  10. package/README.md +25 -0
  11. package/dist/hooks/audit-provision.js +2 -2
  12. package/dist/hooks/batch-guard.js +7 -7
  13. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-2G5F2YCD.js} +3 -3
  14. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3LE6VLSR.js} +17 -6
  15. package/dist/hooks/{chunk-J3KSS7II.js → chunk-4BHUPFKF.js} +1 -1
  16. package/dist/hooks/{chunk-EAQEGME7.js → chunk-5SJSSCJG.js} +1 -1
  17. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  18. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-7KMN5QB5.js} +3 -3
  19. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  20. package/dist/hooks/{chunk-STDSEPWU.js → chunk-EMB55BDF.js} +4 -4
  21. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-JXQBK323.js} +2674 -187
  23. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-MSYSUSY6.js} +5 -5
  24. package/dist/hooks/{chunk-EBSHN25W.js → chunk-MU4LJFIV.js} +4 -4
  25. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-QL6QTPWN.js} +3 -3
  26. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-XER6SSIB.js} +91 -18
  27. package/dist/hooks/{chunk-U32VSRNG.js → chunk-YW452ZAX.js} +89 -34
  28. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-YZXYOAZ3.js} +202 -55
  29. package/dist/hooks/codex-hook-adapter.js +15 -15
  30. package/dist/hooks/duplicate-read-guard.js +8 -8
  31. package/dist/hooks/format-edited.js +5 -5
  32. package/dist/hooks/inputs.json +288 -229
  33. package/dist/hooks/investigation-guard.js +8 -8
  34. package/dist/hooks/pretool-guard.js +14 -14
  35. package/dist/hooks/session-lang.js +69 -16
  36. package/dist/hooks/stop-guard.js +13 -38
  37. package/dist/josh.js +62 -9
  38. package/package.json +4 -3
  39. package/prompts/review.md +22 -0
  40. package/scripts/agent/claude-agent-argv.ts +13 -1
  41. package/scripts/auth/auth-setup-cli.ts +101 -0
  42. package/scripts/auth/claude-token-output.ts +78 -0
  43. package/scripts/auth/claude-token.ts +68 -0
  44. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  45. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  46. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  47. package/scripts/backlog/backlog-drive-named.ts +4 -2
  48. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  49. package/scripts/backlog/backlog-drive.ts +16 -8
  50. package/scripts/backlog/backlog-overlap.ts +22 -5
  51. package/scripts/backlog/backlog-ready.ts +1 -3
  52. package/scripts/backlog/backlogrun-args.ts +13 -2
  53. package/scripts/backlog/backlogrun-cli.ts +91 -19
  54. package/scripts/backlog/backlogrun-launch.ts +24 -0
  55. package/scripts/backlog/backlogrun-replace.ts +166 -0
  56. package/scripts/backlog/backlogrun-stop.ts +178 -0
  57. package/scripts/behavior/behavior-cli.ts +6 -1
  58. package/scripts/doctor/doctor-io.ts +60 -9
  59. package/scripts/doctor/doctor.ts +20 -1
  60. package/scripts/document/document-byte-budget.ts +3 -3
  61. package/scripts/epic/epic-bundle-cli.ts +8 -8
  62. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  63. package/scripts/epic/epic-bundle.ts +39 -4
  64. package/scripts/exports/unused-members-cli.ts +2 -1
  65. package/scripts/followup/followup-checks-event.ts +82 -0
  66. package/scripts/followup/git-followup-finish.ts +16 -2
  67. package/scripts/followup/git-followup-workflow.ts +5 -2
  68. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  69. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  70. package/scripts/followup/git-pr-followup.ts +4 -1
  71. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  72. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  73. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  74. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  75. package/scripts/fresh-machine/fresh-machine-patterns.ts +130 -0
  76. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  77. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  78. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  79. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  80. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  81. package/scripts/gate/gate-report.ts +70 -15
  82. package/scripts/gate/project-checks.ts +13 -2
  83. package/scripts/gate/scoped-green.ts +2 -1
  84. package/scripts/gate/verification-gate.ts +4 -3
  85. package/scripts/gh/gh-author-association.ts +14 -0
  86. package/scripts/gh/git-gh-conditional.ts +66 -0
  87. package/scripts/gh/git-gh-exec.ts +1 -1
  88. package/scripts/gh/git-gh-issue-read.ts +6 -3
  89. package/scripts/gh/git-gh-issue-write.ts +5 -1
  90. package/scripts/gh/git-gh-issue.ts +4 -1
  91. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  92. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  93. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  94. package/scripts/gh/git-pr-checks.ts +36 -2
  95. package/scripts/git/git-command.ts +16 -4
  96. package/scripts/git/git-schemas.ts +1 -0
  97. package/scripts/git/git-spawn-sync.ts +1 -1
  98. package/scripts/hooks/pretool-guard.ts +3 -1
  99. package/scripts/init/hook-command-rewrite.ts +31 -0
  100. package/scripts/init/init-actions.ts +4 -1
  101. package/scripts/init/init-ai-copy.ts +33 -10
  102. package/scripts/init/init-basic.ts +8 -3
  103. package/scripts/init/init-logic-toolchain.ts +76 -0
  104. package/scripts/init/init-logic.ts +2 -0
  105. package/scripts/init/init.ts +5 -23
  106. package/scripts/init/project-profile.ts +36 -7
  107. package/scripts/init/start-exec.ts +27 -1
  108. package/scripts/init/start-plan.ts +15 -0
  109. package/scripts/init/start-steps.ts +46 -0
  110. package/scripts/init/start.ts +1 -0
  111. package/scripts/init/workflow-mark.ts +35 -0
  112. package/scripts/issue/issue-ack-cli.ts +84 -0
  113. package/scripts/issue/issue-labels.ts +40 -8
  114. package/scripts/issue/issue-state-cli.ts +14 -4
  115. package/scripts/issue/issue-watch-diff.ts +152 -0
  116. package/scripts/issue/issue-watch-gate.ts +81 -0
  117. package/scripts/issue/issue-watch-hook.ts +54 -0
  118. package/scripts/issue/issue-watch.ts +240 -0
  119. package/scripts/issue/measure-rerun-cli.ts +3 -4
  120. package/scripts/josh/josh-command-map.ts +4 -0
  121. package/scripts/josh/josh-commands-backlog.ts +2 -2
  122. package/scripts/josh/josh-commands-development.ts +1 -1
  123. package/scripts/josh/josh-commands-issue.ts +7 -0
  124. package/scripts/josh/josh-commands-lane.ts +11 -2
  125. package/scripts/josh/josh-commands-project.ts +24 -0
  126. package/scripts/josh/josh-verdict.ts +33 -4
  127. package/scripts/lane/claude-lane-broker.ts +142 -0
  128. package/scripts/lane/lane-await.ts +6 -2
  129. package/scripts/lane/lane-cache.ts +37 -7
  130. package/scripts/lane/lane-capacity.ts +8 -6
  131. package/scripts/lane/lane-child-invocation.ts +1 -0
  132. package/scripts/lane/lane-cli.ts +15 -1
  133. package/scripts/lane/lane-close.ts +15 -6
  134. package/scripts/lane/lane-dispatch.ts +36 -10
  135. package/scripts/lane/lane-environment.ts +1 -1
  136. package/scripts/lane/lane-launch-cli.ts +13 -4
  137. package/scripts/lane/lane-ledger.ts +21 -3
  138. package/scripts/lane/lane-measure-cli.ts +10 -2
  139. package/scripts/lane/lane-open.ts +9 -1
  140. package/scripts/lane/lane-preserve.ts +49 -0
  141. package/scripts/lane/lane-reap.ts +20 -5
  142. package/scripts/lane/lane-registry.ts +6 -0
  143. package/scripts/lane/lane-restart-cli.ts +51 -0
  144. package/scripts/lane/lane-restart.ts +175 -0
  145. package/scripts/lane/{openai-review-broker.ts → lane-session-broker.ts} +106 -28
  146. package/scripts/lane/lane-spent.ts +195 -0
  147. package/scripts/lane/lane-stage-stats.ts +27 -3
  148. package/scripts/lane/lane-stop-stats.ts +96 -0
  149. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  150. package/scripts/lane/process-tools.ts +31 -0
  151. package/scripts/lib/buffered-process.ts +36 -2
  152. package/scripts/lib/pnpm-reach.ts +27 -0
  153. package/scripts/lib/status-icons.ts +5 -1
  154. package/scripts/lint/lint-parallel.ts +19 -2
  155. package/scripts/observations/observation-ledger-home.ts +1 -0
  156. package/scripts/review/review-attest-cli.ts +21 -6
  157. package/scripts/review/review-brief-cli.ts +2 -2
  158. package/scripts/review/review-comment-round.ts +114 -0
  159. package/scripts/review/review-record-cli.ts +39 -17
  160. package/scripts/rules/implementation-cut.ts +3 -3
  161. package/scripts/rules/pre-gate-cut.ts +1 -1
  162. package/scripts/run/board/run-board-checks.ts +42 -0
  163. package/scripts/run/board/run-board-cli.ts +2 -0
  164. package/scripts/run/board/run-board-every.ts +3 -1
  165. package/scripts/run/board/run-board-fit.ts +16 -5
  166. package/scripts/run/board/run-board-fresh.ts +64 -0
  167. package/scripts/run/board/run-board-header.ts +129 -53
  168. package/scripts/run/board/run-board-labels.ts +47 -4
  169. package/scripts/run/board/run-board-lane-event.ts +66 -0
  170. package/scripts/run/board/run-board-layout.ts +57 -9
  171. package/scripts/run/board/run-board-now.ts +234 -0
  172. package/scripts/run/board/run-board-operation-read.ts +75 -0
  173. package/scripts/run/board/run-board-operation.ts +150 -0
  174. package/scripts/run/board/run-board-phase.ts +26 -1
  175. package/scripts/run/board/run-board-plan.ts +4 -6
  176. package/scripts/run/board/run-board-read.ts +49 -3
  177. package/scripts/run/board/run-board-reason.ts +124 -0
  178. package/scripts/run/board/run-board-render-checks.ts +55 -0
  179. package/scripts/run/board/run-board-render-legend.ts +12 -3
  180. package/scripts/run/board/run-board-render-slot.ts +2 -2
  181. package/scripts/run/board/run-board-render-tail.ts +128 -0
  182. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  183. package/scripts/run/board/run-board-render-title.ts +58 -0
  184. package/scripts/run/board/run-board-render.ts +96 -80
  185. package/scripts/run/board/run-board-row-yield.ts +32 -0
  186. package/scripts/run/board/run-board-setup.ts +61 -13
  187. package/scripts/run/board/run-board-state.ts +10 -0
  188. package/scripts/run/board/run-board-status.ts +66 -7
  189. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  190. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  191. package/scripts/run/board/run-board-thanks.ts +53 -0
  192. package/scripts/run/board/run-board-tick.ts +50 -21
  193. package/scripts/run/board/run-board-usual.ts +44 -0
  194. package/scripts/run/carry/run-carry-args.ts +9 -1
  195. package/scripts/run/carry/run-carry-cli.ts +2 -45
  196. package/scripts/run/carry/run-carry-end.ts +73 -0
  197. package/scripts/run/carry/run-carry.ts +8 -1
  198. package/scripts/run/entry/run-entry-cli.ts +5 -0
  199. package/scripts/run/event/run-event-checks.ts +54 -0
  200. package/scripts/run/event/run-event-render.ts +2 -0
  201. package/scripts/run/event/run-event-scope.ts +1 -0
  202. package/scripts/run/event/run-event-setup.ts +55 -11
  203. package/scripts/run/event/run-event-stream.ts +27 -11
  204. package/scripts/run/merge/run-merge-cli.ts +7 -4
  205. package/scripts/run/merge/run-merge-ship.ts +61 -0
  206. package/scripts/run/merge/run-merge-steps.ts +7 -14
  207. package/scripts/run/merge/run-merge-token.ts +16 -2
  208. package/scripts/run/run-headless.ts +1 -0
  209. package/scripts/run/run-step.ts +2 -0
  210. package/scripts/run/ship/run-ship-cli.ts +22 -2
  211. package/scripts/run/ship/run-ship-return.ts +31 -13
  212. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  213. package/scripts/run/ship/run-ship-stage.ts +1 -0
  214. package/scripts/run/wake/run-wake-await.ts +130 -0
  215. package/scripts/run/wake/run-wake-cli.ts +60 -71
  216. package/scripts/run/wake/run-wake-describe.ts +34 -2
  217. package/scripts/run/wake/run-wake-driver.ts +11 -4
  218. package/scripts/run/wake/run-wake-session.ts +20 -3
  219. package/scripts/run/wake/run-wake-stop.ts +37 -0
  220. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  221. package/scripts/sync/claude-settings-merge.ts +144 -0
  222. package/scripts/sync/sync-ai-files.ts +28 -3
  223. package/scripts/sync/sync.ts +15 -0
  224. package/scripts/test/project-test-script.ts +73 -0
  225. package/scripts/test/skip-marker.ts +14 -1
  226. package/scripts/test/test-related.ts +3 -2
  227. package/scripts/test/test-unit-guard.ts +78 -14
  228. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  229. package/scripts/uninstall/distributed-files.ts +92 -0
  230. package/scripts/uninstall/uninstall-plan.ts +119 -0
  231. package/scripts/uninstall/uninstall-report.ts +61 -0
  232. package/scripts/uninstall/uninstall.ts +206 -0
  233. package/templates/AGENTS.basic.md +4 -0
  234. package/templates/GEMINI.basic.md +6 -0
  235. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  236. package/scripts/run/board/run-board-track.ts +0 -22
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { git_spawn_sync } from '#scripts/git/git-spawn-sync'
4
+ import { pnpm_reach } from '#scripts/lib/pnpm-reach'
4
5
  import { read_spawn_stdout } from '#scripts/lib/spawn-exit'
5
- import { PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
6
+ import { COMMAND_TIMEOUT_MS, PROBE_TIMEOUT_MS } from '#scripts/lib/timeouts'
6
7
  import { execaSync } from 'execa'
7
8
  import { doctor_logic } from './doctor-logic'
8
9
 
@@ -19,22 +20,45 @@ const AUTO_MERGE_WORKFLOW_PATH = '.github/workflows/dependabot-auto-merge.yml'
19
20
  // consumer's own auto-merge workflow which does creates the same one kit's does.
20
21
  const AUTO_MERGE_COMMAND = 'gh pr merge --auto'
21
22
  const YAML_COMMENT_PREFIX = '#'
23
+ const UNKNOWN_PNPM: PnpmProbe = { state: 'unknown' }
22
24
 
23
- // Resolve the `josh` that the shell would run — the first match on PATH, via the platform's
24
- // lookup command (`where` on Windows, `which` elsewhere). Undefined when the lookup fails or
25
- // prints nothing (no `josh` on PATH, e.g. inside the kit repo where only `pnpm josh` runs).
26
- function resolve_path_josh(): string | undefined {
25
+ // What the platform's lookup command said about a name: whether the command itself could be
26
+ // started, and the first match it printed.
27
+ interface PathLookup {
28
+ did_run: boolean
29
+ first_path: string | undefined
30
+ }
31
+
32
+ // Ask the platform's lookup command (`where` on Windows, `which` elsewhere) for `bin` on PATH.
33
+ function look_up_on_path(bin: string): PathLookup {
27
34
  const lookup = doctor_logic.path_lookup_command(process.platform)
28
- const result = execaSync(lookup, [JOSH_BIN], { reject: false })
35
+ const result = execaSync(lookup, [bin], { reject: false })
36
+
37
+ return {
38
+ did_run: !pnpm_reach.is_not_found(result),
39
+ first_path: doctor_logic.first_path_line(read_spawn_stdout(result)),
40
+ }
41
+ }
42
+
43
+ // Resolve the `josh` that the shell would run — the first match on PATH. Undefined when the lookup
44
+ // fails or prints nothing (no `josh` on PATH, e.g. inside the kit repo where only `pnpm josh` runs).
45
+ function resolve_path_josh(): string | undefined {
46
+ return look_up_on_path(JOSH_BIN).first_path
47
+ }
48
+
49
+ // Whether the lookup command ran and found no `bin`. A lookup command that is itself absent proves
50
+ // nothing about `bin`, so it is not an answer.
51
+ function is_off_path(bin: string): boolean {
52
+ const { did_run, first_path } = look_up_on_path(bin)
29
53
 
30
- return doctor_logic.first_path_line(read_spawn_stdout(result))
54
+ return did_run && first_path === undefined
31
55
  }
32
56
 
33
57
  // Resolve the pnpm-global `josh` bin path. Undefined when pnpm is missing or the global bin does
34
58
  // not contain a `josh` (kit not installed globally). `pnpm bin -g` prepends a `[WARN]` line to
35
59
  // stdout inside a project, so the bin directory is extracted via first_path_line.
36
60
  function resolve_pnpm_global_josh(): string | undefined {
37
- const result = execaSync('pnpm', ['bin', '-g'], { reject: false })
61
+ const result = execaSync(pnpm_reach.PNPM, ['bin', '-g'], { reject: false })
38
62
  const bin_directory = doctor_logic.first_path_line(read_spawn_stdout(result))
39
63
  if (bin_directory === undefined) return undefined
40
64
  const candidate = path.join(bin_directory, JOSH_BIN)
@@ -42,6 +66,29 @@ function resolve_pnpm_global_josh(): string | undefined {
42
66
  return existsSync(candidate) ? candidate : undefined
43
67
  }
44
68
 
69
+ // A spawn that found no executable proves pnpm is off PATH, but Windows never reports one: an
70
+ // unresolved `pnpm` is handed to `cmd.exe`, which starts, prints "'pnpm' is not recognized" and exits
71
+ // 1. So a pnpm that did not answer is also looked up by name, which says the same thing everywhere.
72
+ function is_pnpm_missing(result: { code?: string | undefined }): boolean {
73
+ return pnpm_reach.is_not_found(result) || is_off_path(pnpm_reach.PNPM)
74
+ }
75
+
76
+ // Whether this process can start `pnpm`, and which one answers. `josh` runs its checks as
77
+ // `pnpm josh <check>` children, so a `pnpm` this process cannot see fails every one of them before
78
+ // it starts; asking here is what lets a user learn it before a gate does. `missing` is
79
+ // kept apart from `unknown`: a pnpm that is on PATH and failed is a different fault with a
80
+ // different fix.
81
+ function probe_pnpm(): PnpmProbe {
82
+ const result = execaSync(pnpm_reach.PNPM, ['--version'], {
83
+ reject: false,
84
+ timeout: COMMAND_TIMEOUT_MS,
85
+ })
86
+ const version = read_spawn_stdout(result).trim()
87
+ if (version !== '' && result.exitCode === 0) return { state: 'found', version }
88
+
89
+ return is_pnpm_missing(result) ? { state: 'missing' } : UNKNOWN_PNPM
90
+ }
91
+
45
92
  // Whether the working directory is provably *outside* a git work tree. Purely local — no network,
46
93
  // and it does not depend on `gh` being installed or authenticated, which is what makes it usable to
47
94
  // decide whether a repository-scoped check applies at all.
@@ -192,7 +239,11 @@ function has_auto_merge_workflow(start: string, boundary?: string): boolean {
192
239
  type GitTopLevel =
193
240
  { state: 'inside'; top_level: string } | { state: 'outside' } | { state: 'undetermined' }
194
241
 
242
+ // What asking `pnpm --version` from this process established.
243
+ type PnpmProbe = { state: 'found'; version: string } | { state: 'missing' } | { state: 'unknown' }
244
+
195
245
  const doctor_io = {
246
+ probe_pnpm,
196
247
  resolve_path_josh,
197
248
  resolve_pnpm_global_josh,
198
249
  has_distributed_dependabot_config,
@@ -200,5 +251,5 @@ const doctor_io = {
200
251
  resolve_git_top_level,
201
252
  }
202
253
 
203
- export type { GitTopLevel }
254
+ export type { GitTopLevel, PnpmProbe }
204
255
  export { doctor_io }
@@ -5,12 +5,13 @@ import { fileURLToPath } from 'node:url'
5
5
  import { repo_discovery } from '#scripts/discovery/repo-discovery'
6
6
  import { gh_spawn } from '#scripts/gh/gh-spawn'
7
7
  import { PROJECT_ROOT } from '#scripts/init/init-paths'
8
+ import { pnpm_reach } from '#scripts/lib/pnpm-reach'
8
9
  import { auto_merge_setting } from '#scripts/repo/auto-merge-setting'
9
10
  import { ruleset_check } from '#scripts/repo/ruleset-check'
10
11
  import { security_updates } from '#scripts/security/security-updates'
11
12
  import { running_binary } from '#scripts/version/running-binary'
12
13
  import { doctor_consumer } from './doctor-consumer'
13
- import { doctor_io, type GitTopLevel } from './doctor-io'
14
+ import { doctor_io, type GitTopLevel, type PnpmProbe } from './doctor-io'
14
15
  import { doctor_logic } from './doctor-logic'
15
16
  import { doctor_ports } from './doctor-ports'
16
17
 
@@ -43,10 +44,12 @@ interface DoctorContext {
43
44
  running_path: string
44
45
  path_josh: string | undefined
45
46
  global_josh: string | undefined
47
+ pnpm: PnpmProbe
46
48
  }
47
49
 
48
50
  function gather_context(): DoctorContext {
49
51
  return {
52
+ pnpm: doctor_io.probe_pnpm(),
50
53
  running_version: running_binary.read_running_version(SELF_DIR),
51
54
  running_path: running_binary.running_package_directory(SELF_DIR),
52
55
  path_josh: doctor_io.resolve_path_josh(),
@@ -54,13 +57,28 @@ function gather_context(): DoctorContext {
54
57
  }
55
58
  }
56
59
 
60
+ function describe_pnpm(probe: PnpmProbe): string {
61
+ if (probe.state === 'found') return probe.version
62
+
63
+ return probe.state === 'missing' ? NOT_ON_PATH : UNKNOWN
64
+ }
65
+
57
66
  function print_report(ctx: DoctorContext): void {
58
67
  console.info('josh doctor')
59
68
  console.info(` Running: ${ctx.running_version ?? UNKNOWN} (${ctx.running_path})`)
60
69
  console.info(` On PATH: ${ctx.path_josh ?? NOT_ON_PATH}`)
70
+ console.info(` pnpm: ${describe_pnpm(ctx.pnpm)}`)
61
71
  console.info(` pnpm global: ${ctx.global_josh ?? UNKNOWN}`)
62
72
  }
63
73
 
74
+ // Said here rather than left to the gate: every check `josh` runs is a `pnpm` child, so without
75
+ // pnpm on this process's PATH each of them fails before it starts.
76
+ function report_pnpm_reach(ctx: DoctorContext): void {
77
+ if (ctx.pnpm.state !== 'missing') return
78
+ console.info('')
79
+ console.info(`⚠ ${pnpm_reach.NOT_ON_PATH_NOTICE}`)
80
+ }
81
+
64
82
  // Remove a confirmed stale kit shim so the pnpm-global josh reclaims PATH precedence. Any other
65
83
  // shadowing binary is left in place and reported for manual review.
66
84
  function reclaim_shim(path_josh: string, global_josh: string): void {
@@ -207,6 +225,7 @@ function consumer_root(git: GitTopLevel): string {
207
225
  function run_reports(ctx: DoctorContext, git: GitTopLevel, options: DoctorOptions): void {
208
226
  print_report(ctx)
209
227
  report_path_diagnosis(ctx, options.is_fix)
228
+ report_pnpm_reach(ctx)
210
229
  doctor_consumer.report_consumer_setup(consumer_root(git))
211
230
  report_repository_map(git)
212
231
  if (options.show_ports) report_port_seeds(git)
@@ -74,9 +74,9 @@ const DOCUMENT_BYTE_BUDGET: ReadonlyArray<DocumentBudget> = [
74
74
  { path: '.claude/skills/workflow-commands/target-repository.md', bytes: 4096 },
75
75
  { path: '.claude/skills/workflow-commands/working-tree-hold.md', bytes: 8192 },
76
76
  { path: 'docs/josh-commands-automation.md', bytes: 53_248 },
77
- { path: 'docs/josh-commands-run.md', bytes: 69_632 },
78
- { path: 'docs/josh-commands-backlog.md', bytes: 57_344 },
79
- { path: 'docs/josh-commands.md', bytes: 49_152 },
77
+ { path: 'docs/josh-commands-run.md', bytes: 77_824 },
78
+ { path: 'docs/josh-commands-backlog.md', bytes: 61_440 },
79
+ { path: 'docs/josh-commands.md', bytes: 53_248 },
80
80
  { path: 'docs/labels-and-run-states.md', bytes: 8192 },
81
81
  { path: 'prompts/coding-standards.md', bytes: 8192 },
82
82
  { path: 'prompts/collaboration-workflow.md', bytes: 8192 },
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env tsx
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { git_gh_command } from '#scripts/gh/git-gh-command'
4
+ import { issue_label_schema } from '#scripts/git/git-schemas'
4
5
  import { cutoff_of, type ScanCutoff } from '#scripts/git/listing-cutoff'
5
6
  import { parse_json_array_or_undefined } from '#scripts/git/parse-json-array'
6
7
  import { issue_cite } from '#scripts/issue/issue-cite'
@@ -14,6 +15,7 @@ import {
14
15
  type BacklogIssue,
15
16
  type BundleAction,
16
17
  type BundleDecision,
18
+ type MembershipContext,
17
19
  } from './epic-bundle'
18
20
  import { epic_bundle_evidence } from './epic-bundle-evidence'
19
21
  import { epic_bundle_gaps } from './epic-bundle-gaps'
@@ -53,6 +55,7 @@ const UNKNOWN_REPO_MESSAGE =
53
55
  // describing one it does.
54
56
  const backlog_schema = epic_schema.extend({
55
57
  title: z.string().nullable(),
58
+ labels: z.array(issue_label_schema).default([]),
56
59
  blocked_by_count: z.number().nullish(),
57
60
  })
58
61
 
@@ -62,10 +65,6 @@ type BacklogRow = z.infer<typeof backlog_schema>
62
65
  // task-list read would answer differently the first time that shape moved.
63
66
  const { build_epic_index } = epic_index
64
67
 
65
- function to_epic_field(epic: number | undefined): { epic?: number } {
66
- return epic === undefined ? {} : { epic }
67
- }
68
-
69
68
  // Undefined when the read failed, which the caller reports — as opposed to an issue that genuinely
70
69
  // declares no relations, which is an empty array.
71
70
  async function epic_issue_relations(
@@ -139,18 +138,19 @@ interface FetchedBacklog {
139
138
  // One listing row as the bundler reads it. Split from the fetch so that function stays a list of
140
139
  // requests rather than a request list plus a row mapping.
141
140
  function to_backlog_issue(
142
- row: { number: number; title?: string | null; body?: string | null },
143
- context: { repo: string; epics: Map<number, number>; epic_numbers: ReadonlySet<number> },
141
+ row: Pick<BacklogRow, 'number' | 'title' | 'body' | 'labels'>,
142
+ context: MembershipContext & { repo: string },
144
143
  blocked_by: ReadonlyArray<IssueReference>,
145
144
  ): BacklogIssue {
145
+ const labels = row.labels.map((label) => label.name)
146
+
146
147
  return {
147
148
  number: row.number,
148
149
  repo: context.repo,
149
150
  title: row.title ?? '',
150
151
  body: row.body ?? '',
151
152
  blocked_by,
152
- is_epic: context.epic_numbers.has(row.number),
153
- ...to_epic_field(context.epics.get(row.number)),
153
+ ...epic_bundle.membership_of(row.number, labels, context),
154
154
  }
155
155
  }
156
156
 
@@ -1,7 +1,7 @@
1
1
  import { git_gh_command } from '#scripts/gh/git-gh-command'
2
2
  import { bounded_pool } from '#scripts/lib/bounded-pool'
3
3
  import { epic_audit_logic } from './epic-audit'
4
- import type { BacklogIssue } from './epic-bundle'
4
+ import { epic_bundle, type BacklogIssue } from './epic-bundle'
5
5
  import { epic_issue, type EpicIssue } from './epic-issue'
6
6
 
7
7
  // Candidates the open backlog cannot show, read from the subject's own prose.
@@ -64,15 +64,12 @@ interface ReferencedContext {
64
64
  }
65
65
 
66
66
  function to_backlog_issue(issue: EpicIssue, context: ReferencedContext): BacklogIssue {
67
- const epic = context.epics.get(issue.number)
68
-
69
67
  return {
70
68
  number: issue.number,
71
69
  repo: context.repo,
72
70
  body: issue.body,
73
71
  blocked_by: epic_issue.blocker_references_of(issue, context.repo),
74
- is_epic: context.epic_numbers.has(issue.number),
75
- ...(epic !== undefined && { epic }),
72
+ ...epic_bundle.membership_of(issue.number, epic_issue.label_names(issue), context),
76
73
  }
77
74
  }
78
75
 
@@ -1,4 +1,5 @@
1
1
  import { issue_cite } from '#scripts/issue/issue-cite'
2
+ import { has_label_name, RELEASE_LABEL } from '#scripts/issue/issue-labels'
2
3
  import { epic_audit_logic } from './epic-audit'
3
4
  import { epic_graph } from './epic-graph'
4
5
  import type { DependencyLink } from './epic-parse'
@@ -31,6 +32,11 @@ interface BacklogIssue {
31
32
  // its parent, so without this every child would find its own epic as a candidate and be told to
32
33
  // bundle with it.
33
34
  is_epic?: boolean
35
+ // Whether this issue is the release issue. `issue:file --release` records every change awaiting
36
+ // publication as its blocker, so the relation is mechanical and says nothing about the work
37
+ // belonging together — read as a signal, it proposed one epic for the release and every
38
+ // unrelated change waiting on it.
39
+ is_release?: boolean
34
40
  // Repository-qualified, not bare numbers. Issue numbers are unique per
35
41
  // repository, so a blocker read as `40` alone matched any candidate numbered 40 — including one in
36
42
  // a different repository entirely. That produced a bundle nobody's data supported, and, through
@@ -79,6 +85,36 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
79
85
  return names_as_blocker(subject, other) || names_as_blocker(other, subject)
80
86
  }
81
87
 
88
+ // The epic view an issue is placed against: which epic tracks each issue, and the epics themselves.
89
+ interface MembershipContext {
90
+ epics: ReadonlyMap<number, number>
91
+ epic_numbers: ReadonlySet<number>
92
+ }
93
+
94
+ type Membership = Pick<BacklogIssue, 'epic' | 'is_epic' | 'is_release'>
95
+
96
+ // The fields that decide whether and where an issue takes part in a bundle. Single-sourced so the
97
+ // listing read and the per-reference read cannot disagree about which issue is an epic or the
98
+ // release one.
99
+ function membership_of(
100
+ number: number,
101
+ labels: ReadonlyArray<string>,
102
+ context: MembershipContext,
103
+ ): Membership {
104
+ const epic = context.epics.get(number)
105
+
106
+ return {
107
+ is_epic: context.epic_numbers.has(number),
108
+ is_release: has_label_name(labels, RELEASE_LABEL),
109
+ ...(epic !== undefined && { epic }),
110
+ }
111
+ }
112
+
113
+ // Neither an epic nor the release issue takes part in a bundle, on either side.
114
+ function is_excluded(issue: BacklogIssue): boolean {
115
+ return issue.is_epic === true || issue.is_release === true
116
+ }
117
+
82
118
  // A strong signal, and only a strong signal.
83
119
  //
84
120
  // Similar titles are deliberately not one. "Related" expands without limit, and a threshold is what
@@ -87,9 +123,7 @@ function has_recorded_dependency(subject: BacklogIssue, other: BacklogIssue): bo
87
123
  function is_strong_signal(subject: BacklogIssue, other: BacklogIssue): boolean {
88
124
  // Neither side. Asked about an epic, the one-sided check found every one of its children through
89
125
  // their own `parent: #N` line and proposed bundling a container with its contents.
90
- if (subject.number === other.number || subject.is_epic === true || other.is_epic === true) {
91
- return false
92
- }
126
+ if (subject.number === other.number || is_excluded(subject) || is_excluded(other)) return false
93
127
 
94
128
  return has_mutual_reference(subject, other) || has_recorded_dependency(subject, other)
95
129
  }
@@ -295,10 +329,11 @@ const epic_bundle = {
295
329
  already_tracked_reason,
296
330
  has_recorded_dependency,
297
331
  is_strong_signal,
332
+ membership_of,
298
333
  decide_bundle,
299
334
  bundle_dependency_links,
300
335
  bundle_children,
301
336
  }
302
337
 
303
- export type { BacklogIssue, BundleAction, BundleDecision }
338
+ export type { BacklogIssue, BundleAction, BundleDecision, MembershipContext }
304
339
  export { epic_bundle }
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
5
5
  import { project_checks } from '#scripts/gate/project-checks'
6
+ import { NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
6
7
  import type { NamespaceMember } from './namespace-members'
7
8
 
8
9
  const CHECK = 'exports'
@@ -13,7 +14,7 @@ const FAIL_EXIT_CODE = 1
13
14
  // **Not a skip notice**. `SKIP_MARKER` means "a check passed without running",
14
15
  // and the gate withholds its green record on it — so a consumer, where this notice prints on every
15
16
  // run, never got a green record. Out of scope by design is not unverified, so the marker stays out.
16
- const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself — nothing to check in a consumer project.`
17
+ const CONSUMER_NOTICE = `josh ${CHECK}: checks only the kit repository itself — ${NOTHING_TO_CHECK_MARKER} in a consumer project.`
17
18
 
18
19
  function format_member(root: string, member: NamespaceMember): string {
19
20
  return `${path.relative(root, member.file)}:${String(member.line)} ${member.namespace}.${member.member}`
@@ -0,0 +1,82 @@
1
+ import type { SnapshotObserver } from '#scripts/gh/git-pr-checks'
2
+ import { is_coderabbit_check, is_coderabbit_rate_limited } from '#scripts/gh/git-pr-checks-eval'
3
+ import {
4
+ CHECK_STATUS_FAIL,
5
+ CHECK_STATUS_PASS,
6
+ CHECK_STATUS_PENDING,
7
+ type PrStateSnapshot,
8
+ type RollupCheck,
9
+ } from '#scripts/gh/git-pr-checks-parse'
10
+ import {
11
+ run_event_checks,
12
+ type CodeRabbitState,
13
+ type PrChecks,
14
+ } from '#scripts/run/event/run-event-checks'
15
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
16
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
17
+
18
+ // What `followup`'s check wait tells the run's event stream (joshuafolkken/kit#3770): the CI counts and
19
+ // CodeRabbit's state, one `pr-checks` line each time they change, so `run:board` draws a child waiting
20
+ // in followup from the stream alone. CodeRabbit is counted apart — it has a state of its own and does
21
+ // not gate the merge.
22
+
23
+ const PR_NUMBER_PATTERN = /\/pull\/(?<number>\d+)/u
24
+ const { PR_CHECKS } = run_event_stream.EVENT_KIND
25
+
26
+ function count_of(checks: ReadonlyArray<RollupCheck>, status: string): number {
27
+ return checks.filter((check) => check.status === status).length
28
+ }
29
+
30
+ function coderabbit_state_of(check: RollupCheck | undefined): CodeRabbitState {
31
+ if (check === undefined) return 'none'
32
+ if (check.status === CHECK_STATUS_PENDING) return 'running'
33
+ if (check.status !== CHECK_STATUS_PASS) return 'failed'
34
+
35
+ return is_coderabbit_rate_limited(check) ? 'rate-limited' : 'done'
36
+ }
37
+
38
+ function summary_of(snapshot: PrStateSnapshot, pr: number): PrChecks {
39
+ const ci = snapshot.rollup.filter((check) => !is_coderabbit_check(check.name))
40
+ const coderabbit = snapshot.rollup.find((check) => is_coderabbit_check(check.name))
41
+
42
+ return {
43
+ pr,
44
+ pass: count_of(ci, CHECK_STATUS_PASS),
45
+ pending: count_of(ci, CHECK_STATUS_PENDING),
46
+ fail: count_of(ci, CHECK_STATUS_FAIL),
47
+ coderabbit: coderabbit_state_of(coderabbit),
48
+ }
49
+ }
50
+
51
+ function pr_number_of(pr_url = ''): number | undefined {
52
+ const number = PR_NUMBER_PATTERN.exec(pr_url)?.groups?.['number']
53
+
54
+ return number === undefined ? undefined : Number(number)
55
+ }
56
+
57
+ async function skip_snapshot(): Promise<void> {
58
+ // Nothing names the child or its pull request, so there is no row to report to.
59
+ }
60
+
61
+ // An observer that writes a line only when it differs from the one this wait last wrote; one that
62
+ // does nothing where the issue or the pull request is unknown.
63
+ function observer_of(issue: string | undefined, pr_url: string | undefined): SnapshotObserver {
64
+ const pr = pr_number_of(pr_url)
65
+
66
+ if (issue === undefined || pr === undefined) return skip_snapshot
67
+
68
+ let last_text = ''
69
+
70
+ return async function report_snapshot(snapshot: PrStateSnapshot): Promise<void> {
71
+ const text = run_event_checks.text_of(issue, summary_of(snapshot, pr))
72
+
73
+ if (text === last_text) return
74
+
75
+ last_text = text
76
+ await run_event_stream_emit.emit(PR_CHECKS, text)
77
+ }
78
+ }
79
+
80
+ const followup_checks_event = { observer_of, summary_of }
81
+
82
+ export { followup_checks_event }
@@ -1,4 +1,5 @@
1
1
  import { git_next_issues } from '#scripts/issue/git-next-issues'
2
+ import { issue_watch } from '#scripts/issue/issue-watch'
2
3
  import { review_attest } from '#scripts/review/review-attest'
3
4
  import { review_stamps } from '#scripts/review/review-stamps'
4
5
  import { run_hold } from '#scripts/run/hold/run-hold'
@@ -67,11 +68,24 @@ async function clear_review_target(should_merge: boolean): Promise<void> {
67
68
  }
68
69
  }
69
70
 
70
- // The two review records are one step of the tail: both are cleared by a merged run and neither is
71
- // worth reporting on its own.
71
+ // The issue watch ends with the run it watched: a record left behind would make the next run's hook
72
+ // poll an issue that is no longer its own.
73
+ async function clear_issue_watch(should_merge: boolean): Promise<void> {
74
+ if (!should_merge) return
75
+
76
+ try {
77
+ await issue_watch.clear_here()
78
+ } catch {
79
+ /* the next `josh run:entry` replaces the record */
80
+ }
81
+ }
82
+
83
+ // The review records and the issue watch are one step of the tail: all are cleared by a merged run
84
+ // and none is worth reporting on its own.
72
85
  async function clear_review_records(should_merge: boolean): Promise<void> {
73
86
  clear_round_one_snapshot(should_merge)
74
87
  await clear_review_target(should_merge)
88
+ await clear_issue_watch(should_merge)
75
89
  }
76
90
 
77
91
  // The working-tree hold a typed entry point claims before it starts is released here, on the one seam every `fullrun` — and every child of an `epicrun` or a `queue` —
@@ -3,6 +3,7 @@ import { parseArgs } from 'node:util'
3
3
  import { git_branch } from '#scripts/git/git-branch'
4
4
  import { git_error } from '#scripts/git/git-error'
5
5
  import { issue_number_shape } from '#scripts/issue/issue-number-shape'
6
+ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
6
7
  import { cli_body } from '#scripts/josh/cli-body'
7
8
  import { josh_environment_file } from '#scripts/josh/josh-environment-file'
8
9
  import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
@@ -151,9 +152,11 @@ async function assert_review_attested(should_merge: boolean): Promise<void> {
151
152
 
152
153
  const verdict = await review_attest.check_here()
153
154
 
154
- if (is_merge_permitted(verdict.status)) return
155
+ if (!is_merge_permitted(verdict.status)) throw new Error(review_attest.refusal_message(verdict))
156
+
157
+ const refusal = await issue_watch_gate.review_refusal_here()
155
158
 
156
- throw new Error(review_attest.refusal_message(verdict))
159
+ if (refusal !== undefined) throw new Error(refusal)
157
160
  }
158
161
 
159
162
  // Recording a review round as a prose request leaves the recurrence ledger blind whenever it is
@@ -4,9 +4,11 @@ import {
4
4
  DEFAULT_STABLE_READS,
5
5
  git_pr_checks,
6
6
  WATCH_CONFIRMED_STABLE_READS,
7
+ type SnapshotObserver,
7
8
  } from '#scripts/gh/git-pr-checks'
8
9
  import { is_coderabbit_check } from '#scripts/gh/git-pr-checks-eval'
9
10
  import { CHECK_STATUS_PASS, type PrStateSnapshot } from '#scripts/gh/git-pr-checks-parse'
11
+ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
10
12
  import { json_value } from '#scripts/lib/json-value'
11
13
 
12
14
  // The check wait and the notes read off its snapshot — split out of `git-pr-followup.ts` when it
@@ -75,12 +77,15 @@ async function handle_watch_failure(branch_name: string, error: unknown): Promis
75
77
  // reads the wait still needs. A timed-out watch (`timed_out: true`) never spanned the pending→settled
76
78
  // window, so it must keep the full stable-read window rather than claim the confirmation. A rethrown
77
79
  // failure — the empty-rollup case — never reaches a caller, so it needs no answer here.
78
- async function watch_before_polling(branch_name: string): Promise<boolean> {
80
+ async function watch_before_polling(
81
+ branch_name: string,
82
+ observe: SnapshotObserver | undefined,
83
+ ): Promise<boolean> {
79
84
  console.info('')
80
85
  console.info('📊 Watching PR checks...')
81
86
 
82
87
  try {
83
- const result = await git_gh_command.pr_checks_watch(branch_name)
88
+ const result = await git_gh_command.pr_checks_watch(branch_name, observe)
84
89
 
85
90
  return !result.timed_out
86
91
  } catch (error) {
@@ -95,14 +100,23 @@ async function watch_before_polling(branch_name: string): Promise<boolean> {
95
100
  // once rather than twice — which removes the extra interval `followup` spent re-proving a settled run.
96
101
  // A skipped or fallen-through watch keeps `DEFAULT_STABLE_READS`, and a check that turned red is still
97
102
  // caught: the poll evaluates the full merge gate and throws on failure before any stable read counts.
103
+ // `observe` is shown each poll's snapshot, the watch's included — the run's `pr-checks` line
104
+ // (joshuafolkken/kit#3770).
98
105
  async function run_checks(input: {
99
106
  branch_name: string
100
107
  is_skip_watch: boolean
108
+ observe?: SnapshotObserver
101
109
  }): Promise<PrStateSnapshot> {
102
- const is_watch_confirmed = !input.is_skip_watch && (await watch_before_polling(input.branch_name))
110
+ const is_watch_confirmed =
111
+ !input.is_skip_watch && (await watch_before_polling(input.branch_name, input.observe))
103
112
  const stable_reads = is_watch_confirmed ? WATCH_CONFIRMED_STABLE_READS : DEFAULT_STABLE_READS
104
113
 
105
- return await git_pr_checks.wait_for_pr_success(input.branch_name, stable_reads)
114
+ return await git_pr_checks.wait_for_pr_success(
115
+ input.branch_name,
116
+ stable_reads,
117
+ issue_watch_gate.assert_read_here,
118
+ input.observe,
119
+ )
106
120
  }
107
121
 
108
122
  // Temporary: record every CodeRabbit check that was not passing when the merge gate
@@ -1,5 +1,6 @@
1
1
  import { epic_close } from '#scripts/epic/epic-close'
2
2
  import { git_gh_command } from '#scripts/gh/git-gh-command'
3
+ import { issue_watch_gate } from '#scripts/issue/issue-watch-gate'
3
4
  import { git_notify, type GitNotifyConfig } from '#scripts/notify/git-notify'
4
5
  import { git_followup_cleanup } from './git-followup-cleanup'
5
6
  import { git_followup_issue_close } from './git-followup-issue-close'
@@ -232,8 +233,12 @@ async function run_tail_steps(input: WrapupInput): Promise<void> {
232
233
  ])
233
234
  }
234
235
 
236
+ // The last look at the issue before the merge: the CI poll checked it on every iteration, but a
237
+ // person's update can land between that poll and this step, and only the seconds after this read are
238
+ // left unwatched.
235
239
  async function run_wrapup(input: WrapupInput, log: StageLog): Promise<void> {
236
240
  if (input.should_merge && input.is_merged !== true) {
241
+ await issue_watch_gate.assert_read_here()
237
242
  await git_gh_command.pr_merge(input.branch_name)
238
243
  lap(log, STAGE.merge)
239
244
  }
@@ -10,6 +10,7 @@ import {
10
10
  type TelegramSendInput,
11
11
  type TelegramTaskType,
12
12
  } from '#scripts/notify/telegram-notify'
13
+ import { followup_checks_event } from './followup-checks-event'
13
14
  import { git_followup_merged } from './git-followup-merged'
14
15
  import { git_followup_pending } from './git-followup-pending'
15
16
  import { git_followup_stages, type StageLog } from './git-followup-stages'
@@ -229,6 +230,7 @@ async function run_review_checks(
229
230
  input: FollowupInput,
230
231
  context: TelegramContext,
231
232
  log: StageLog,
233
+ issue_number: string | undefined,
232
234
  ): Promise<{ notes: Array<string>; managed: Array<string> }> {
233
235
  if (input.is_merged === true) return { notes: [], managed: [] }
234
236
 
@@ -238,6 +240,7 @@ async function run_review_checks(
238
240
  const snapshot = await run_checks({
239
241
  branch_name: input.branch_name,
240
242
  is_skip_watch: input.is_skip_watch,
243
+ observe: followup_checks_event.observer_of(issue_number, context.pr_url),
241
244
  })
242
245
 
243
246
  lap(log, STAGE.checks_wait)
@@ -278,7 +281,7 @@ async function run_stages(input: FollowupInput, log: StageLog): Promise<string |
278
281
  const { issue_number, closes_number, context } = await read_run_context(input)
279
282
 
280
283
  lap(log, STAGE.closes_and_context)
281
- const checks = await run_review_checks(input, context, log)
284
+ const checks = await run_review_checks(input, context, log, issue_number)
282
285
  const is_completion_recorded = await notify_unless_recorded(input, issue_number, context, checks)
283
286
 
284
287
  lap(log, STAGE.telegram)