@joshuafolkken/kit 1.1387.0 → 1.1453.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 (278) 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-park.md +2 -0
  5. package/.claude/skills/workflow-commands/backlogrun-steps.md +13 -10
  6. package/.claude/skills/workflow-commands/backlogrun.md +3 -2
  7. package/.claude/skills/workflow-commands/chain-rule.md +4 -2
  8. package/.claude/skills/workflow-commands/fullrun-steps.md +1 -1
  9. package/.claude/skills/workflow-commands/halfrun.md +1 -1
  10. package/.claude/skills/workflow-commands/issue-comments.md +6 -0
  11. package/.claude/skills/workflow-commands/kickoff.md +12 -13
  12. package/.claude/skills/workflow-commands/observation-filing.md +1 -1
  13. package/.claude/skills/workflow-commands/observation-ledger.md +3 -2
  14. package/.claude/skills/workflow-commands/pre-gate-cut.md +5 -3
  15. package/.claude/skills/workflow-commands/prerequisite.md +6 -0
  16. package/.claude/skills/workflow-commands/retrospective.md +5 -5
  17. package/.claude/skills/workflow-commands/split-assessment.md +2 -2
  18. package/README.md +27 -4
  19. package/dist/hooks/audit-provision.js +2 -2
  20. package/dist/hooks/batch-guard.js +7 -7
  21. package/dist/hooks/{chunk-2YSW3CXH.js → chunk-3C4XRYG6.js} +35 -11
  22. package/dist/hooks/{chunk-GJ7JPIHN.js → chunk-5C7L6FRC.js} +2704 -190
  23. package/dist/hooks/{chunk-FZ6FUSBM.js → chunk-723HKZO5.js} +1 -1
  24. package/dist/hooks/{chunk-STDSEPWU.js → chunk-D3NJXN5M.js} +4 -4
  25. package/dist/hooks/{chunk-NEC4AT4Z.js → chunk-DGT54IH5.js} +4 -0
  26. package/dist/hooks/{chunk-LMWBRJIP.js → chunk-EF5GQ6WS.js} +3 -3
  27. package/dist/hooks/{chunk-JBBMAPC4.js → chunk-IX6TTTI6.js} +4 -0
  28. package/dist/hooks/{chunk-IYVOLN6A.js → chunk-LBFYEOXJ.js} +3 -3
  29. package/dist/hooks/{chunk-SL4GQ2XP.js → chunk-MBJYQPEK.js} +106 -23
  30. package/dist/hooks/{chunk-WNFJLBXI.js → chunk-NO4AALP5.js} +3 -3
  31. package/dist/hooks/{chunk-J3KSS7II.js → chunk-OLMO72ON.js} +1 -1
  32. package/dist/hooks/{chunk-DCY2GWHC.js → chunk-SHYUQOIS.js} +5 -5
  33. package/dist/hooks/{chunk-EAQEGME7.js → chunk-UT7YC3N2.js} +1 -1
  34. package/dist/hooks/{chunk-EBSHN25W.js → chunk-W5ZKGTSY.js} +4 -4
  35. package/dist/hooks/{chunk-GNDJ7NJ7.js → chunk-Y3GKIKGW.js} +209 -56
  36. package/dist/hooks/{chunk-U32VSRNG.js → chunk-Z4XI7CZS.js} +89 -34
  37. package/dist/hooks/codex-hook-adapter.js +15 -15
  38. package/dist/hooks/duplicate-read-guard.js +8 -8
  39. package/dist/hooks/format-edited.js +5 -5
  40. package/dist/hooks/inputs.json +261 -201
  41. package/dist/hooks/investigation-guard.js +8 -8
  42. package/dist/hooks/pretool-guard.js +14 -14
  43. package/dist/hooks/session-lang.js +69 -16
  44. package/dist/hooks/stop-guard.js +13 -38
  45. package/dist/josh.js +95 -19
  46. package/package.json +4 -3
  47. package/prompts/collaboration-workflow/issue-template.md +10 -8
  48. package/prompts/review.md +30 -4
  49. package/scripts/agent/claude-agent-argv.ts +13 -1
  50. package/scripts/auth/auth-setup-cli.ts +101 -0
  51. package/scripts/auth/claude-token-output.ts +78 -0
  52. package/scripts/auth/claude-token.ts +68 -0
  53. package/scripts/auto-ok/auto-ok-judge-cli.ts +125 -0
  54. package/scripts/auto-ok/auto-ok-judge-read.ts +95 -0
  55. package/scripts/auto-ok/auto-ok-judge-write.ts +109 -0
  56. package/scripts/auto-ok/auto-ok-judge.ts +67 -0
  57. package/scripts/backlog/backlog-drive-cli.ts +14 -0
  58. package/scripts/backlog/backlog-drive-epic.ts +24 -8
  59. package/scripts/backlog/backlog-drive-launch.ts +3 -0
  60. package/scripts/backlog/backlog-drive-named-offer.ts +7 -2
  61. package/scripts/backlog/backlog-drive-named.ts +4 -2
  62. package/scripts/backlog/backlog-drive-unread.ts +56 -0
  63. package/scripts/backlog/backlog-drive.ts +38 -11
  64. package/scripts/backlog/backlog-overlap.ts +22 -5
  65. package/scripts/backlog/backlog-ready.ts +1 -3
  66. package/scripts/backlog/backlogrun-args.ts +13 -2
  67. package/scripts/backlog/backlogrun-cli.ts +91 -19
  68. package/scripts/backlog/backlogrun-launch.ts +24 -0
  69. package/scripts/backlog/backlogrun-replace.ts +166 -0
  70. package/scripts/backlog/backlogrun-stop.ts +178 -0
  71. package/scripts/behavior/behavior-cli.ts +6 -1
  72. package/scripts/doctor/doctor-io.ts +60 -9
  73. package/scripts/doctor/doctor.ts +20 -1
  74. package/scripts/document/document-byte-budget.ts +3 -3
  75. package/scripts/epic/epic-bundle-cli.ts +8 -8
  76. package/scripts/epic/epic-bundle-referenced.ts +2 -5
  77. package/scripts/epic/epic-bundle.ts +39 -4
  78. package/scripts/exports/unused-members-cli.ts +2 -1
  79. package/scripts/followup/followup-checks-event.ts +82 -0
  80. package/scripts/followup/git-followup-finish.ts +16 -2
  81. package/scripts/followup/git-followup-workflow.ts +5 -2
  82. package/scripts/followup/git-pr-followup-checks.ts +18 -4
  83. package/scripts/followup/git-pr-followup-wrapup.ts +5 -0
  84. package/scripts/followup/git-pr-followup.ts +4 -1
  85. package/scripts/fresh-machine/fresh-machine-cli.ts +204 -0
  86. package/scripts/fresh-machine/fresh-machine-docker.ts +65 -0
  87. package/scripts/fresh-machine/fresh-machine-image.ts +62 -0
  88. package/scripts/fresh-machine/fresh-machine-log.ts +61 -0
  89. package/scripts/fresh-machine/fresh-machine-patterns.ts +175 -0
  90. package/scripts/fresh-machine/fresh-machine-run.ts +215 -0
  91. package/scripts/fresh-machine/fresh-machine-script.ts +45 -0
  92. package/scripts/fresh-machine/fresh-machine-spec.ts +57 -0
  93. package/scripts/fresh-machine/specs/kit-test-backlogrun-python.json +34 -0
  94. package/scripts/fresh-machine/specs/kit-test-backlogrun.json +31 -0
  95. package/scripts/gate/gate-report.ts +70 -15
  96. package/scripts/gate/project-checks.ts +18 -4
  97. package/scripts/gate/scoped-green.ts +2 -1
  98. package/scripts/gate/verification-gate.ts +4 -3
  99. package/scripts/gh/gh-author-association.ts +14 -0
  100. package/scripts/gh/git-gh-conditional.ts +66 -0
  101. package/scripts/gh/git-gh-exec.ts +1 -1
  102. package/scripts/gh/git-gh-issue-read.ts +6 -3
  103. package/scripts/gh/git-gh-issue-rest.ts +1 -0
  104. package/scripts/gh/git-gh-issue-write.ts +5 -1
  105. package/scripts/gh/git-gh-issue.ts +13 -1
  106. package/scripts/gh/git-pr-checks-eval.ts +13 -0
  107. package/scripts/gh/git-pr-checks-parse.ts +8 -1
  108. package/scripts/gh/git-pr-checks-watch.ts +11 -4
  109. package/scripts/gh/git-pr-checks.ts +36 -2
  110. package/scripts/git/git-command.ts +16 -4
  111. package/scripts/git/git-schemas.ts +1 -0
  112. package/scripts/git/git-spawn-sync.ts +1 -1
  113. package/scripts/hooks/pretool-guard.ts +3 -1
  114. package/scripts/init/hook-command-rewrite.ts +31 -0
  115. package/scripts/init/init-actions.ts +7 -4
  116. package/scripts/init/init-ai-copy.ts +34 -11
  117. package/scripts/init/init-basic.ts +9 -4
  118. package/scripts/init/init-bootstrap.ts +6 -2
  119. package/scripts/init/init-dry-run-report.ts +150 -0
  120. package/scripts/init/init-dry-run.ts +189 -0
  121. package/scripts/init/init-install.ts +8 -1
  122. package/scripts/init/init-logic-toolchain.ts +76 -0
  123. package/scripts/init/init-logic.ts +2 -0
  124. package/scripts/init/init.ts +62 -42
  125. package/scripts/init/kit-setup-state.ts +11 -17
  126. package/scripts/init/own-formatter.ts +21 -0
  127. package/scripts/init/project-profile.ts +60 -12
  128. package/scripts/init/start-dry-run.ts +70 -0
  129. package/scripts/init/start-exec.ts +27 -1
  130. package/scripts/init/start-plan.ts +23 -3
  131. package/scripts/init/start-setup-pr.ts +13 -24
  132. package/scripts/init/start-steps.ts +62 -1
  133. package/scripts/init/start.ts +38 -17
  134. package/scripts/init/workflow-mark.ts +35 -0
  135. package/scripts/init/working-tree.ts +116 -0
  136. package/scripts/issue/issue-ack-cli.ts +84 -0
  137. package/scripts/issue/issue-auto-ok.ts +68 -99
  138. package/scripts/issue/issue-file-cli.ts +17 -17
  139. package/scripts/issue/issue-file.ts +1 -6
  140. package/scripts/issue/issue-labels.ts +46 -10
  141. package/scripts/issue/issue-release-cli.ts +69 -23
  142. package/scripts/issue/issue-release.ts +3 -1
  143. package/scripts/issue/issue-state-cli.ts +14 -4
  144. package/scripts/issue/issue-watch-diff.ts +152 -0
  145. package/scripts/issue/issue-watch-gate.ts +81 -0
  146. package/scripts/issue/issue-watch-hook.ts +54 -0
  147. package/scripts/issue/issue-watch.ts +240 -0
  148. package/scripts/issue/measure-rerun-cli.ts +3 -4
  149. package/scripts/josh/josh-command-map.ts +4 -0
  150. package/scripts/josh/josh-commands-ai.ts +11 -0
  151. package/scripts/josh/josh-commands-backlog.ts +2 -2
  152. package/scripts/josh/josh-commands-development.ts +1 -1
  153. package/scripts/josh/josh-commands-issue.ts +10 -3
  154. package/scripts/josh/josh-commands-lane.ts +11 -2
  155. package/scripts/josh/josh-commands-project.ts +26 -2
  156. package/scripts/josh/josh-verdict.ts +33 -4
  157. package/scripts/josh/process-identity.ts +32 -6
  158. package/scripts/lane/claude-lane-broker.ts +142 -0
  159. package/scripts/lane/lane-await.ts +6 -2
  160. package/scripts/lane/lane-broker-server.ts +69 -0
  161. package/scripts/lane/lane-cache.ts +37 -7
  162. package/scripts/lane/lane-capacity.ts +8 -6
  163. package/scripts/lane/lane-child-invocation.ts +1 -0
  164. package/scripts/lane/lane-cli.ts +15 -1
  165. package/scripts/lane/lane-close.ts +15 -6
  166. package/scripts/lane/lane-dispatch.ts +36 -10
  167. package/scripts/lane/lane-environment.ts +1 -1
  168. package/scripts/lane/lane-launch-cli.ts +13 -4
  169. package/scripts/lane/lane-ledger.ts +21 -3
  170. package/scripts/lane/lane-measure-cli.ts +10 -2
  171. package/scripts/lane/lane-open.ts +9 -1
  172. package/scripts/lane/lane-preserve.ts +49 -0
  173. package/scripts/lane/lane-reap.ts +20 -5
  174. package/scripts/lane/lane-registry.ts +6 -0
  175. package/scripts/lane/lane-restart-cli.ts +51 -0
  176. package/scripts/lane/lane-restart.ts +175 -0
  177. package/scripts/lane/lane-session-broker.ts +363 -0
  178. package/scripts/lane/lane-spent.ts +195 -0
  179. package/scripts/lane/lane-stage-stats.ts +27 -3
  180. package/scripts/lane/lane-stop-stats.ts +96 -0
  181. package/scripts/lane/openai-lane-supervisor.ts +2 -2
  182. package/scripts/lane/process-tools.ts +31 -0
  183. package/scripts/lib/buffered-process.ts +36 -2
  184. package/scripts/lib/pnpm-reach.ts +27 -0
  185. package/scripts/lib/status-icons.ts +5 -1
  186. package/scripts/lint/lint-parallel.ts +19 -2
  187. package/scripts/observations/observation-ledger-home.ts +1 -0
  188. package/scripts/observations/observation-ledger-migrate.ts +3 -15
  189. package/scripts/release/release-issue-close.ts +133 -0
  190. package/scripts/release/release-publish.ts +12 -1
  191. package/scripts/repo/repository-labels.ts +11 -1
  192. package/scripts/review/comment-ranges.ts +199 -0
  193. package/scripts/review/review-attest-cli.ts +21 -6
  194. package/scripts/review/review-brief-cli.ts +24 -7
  195. package/scripts/review/review-code-tree.ts +119 -0
  196. package/scripts/review/review-comment-round.ts +114 -0
  197. package/scripts/review/review-record-cli.ts +39 -17
  198. package/scripts/review/review-round2-cli.ts +16 -5
  199. package/scripts/review/review-round2.ts +30 -10
  200. package/scripts/review/review-stamps.ts +22 -1
  201. package/scripts/rules/implementation-cut.ts +3 -3
  202. package/scripts/rules/pre-gate-cut.ts +1 -1
  203. package/scripts/run/board/run-board-checks.ts +42 -0
  204. package/scripts/run/board/run-board-cli.ts +2 -0
  205. package/scripts/run/board/run-board-every.ts +3 -1
  206. package/scripts/run/board/run-board-fit.ts +16 -5
  207. package/scripts/run/board/run-board-fresh.ts +64 -0
  208. package/scripts/run/board/run-board-header.ts +129 -53
  209. package/scripts/run/board/run-board-labels.ts +47 -4
  210. package/scripts/run/board/run-board-lane-event.ts +66 -0
  211. package/scripts/run/board/run-board-layout.ts +57 -9
  212. package/scripts/run/board/run-board-now.ts +234 -0
  213. package/scripts/run/board/run-board-operation-read.ts +75 -0
  214. package/scripts/run/board/run-board-operation.ts +150 -0
  215. package/scripts/run/board/run-board-phase.ts +26 -1
  216. package/scripts/run/board/run-board-plan.ts +4 -6
  217. package/scripts/run/board/run-board-read.ts +49 -3
  218. package/scripts/run/board/run-board-reason.ts +124 -0
  219. package/scripts/run/board/run-board-render-checks.ts +55 -0
  220. package/scripts/run/board/run-board-render-legend.ts +12 -3
  221. package/scripts/run/board/run-board-render-slot.ts +2 -2
  222. package/scripts/run/board/run-board-render-tail.ts +128 -0
  223. package/scripts/run/board/run-board-render-thanks.ts +50 -0
  224. package/scripts/run/board/run-board-render-title.ts +58 -0
  225. package/scripts/run/board/run-board-render.ts +96 -80
  226. package/scripts/run/board/run-board-row-yield.ts +32 -0
  227. package/scripts/run/board/run-board-setup.ts +61 -13
  228. package/scripts/run/board/run-board-state.ts +10 -0
  229. package/scripts/run/board/run-board-status.ts +66 -7
  230. package/scripts/run/board/run-board-thanks-csv.ts +71 -0
  231. package/scripts/run/board/run-board-thanks-read.ts +112 -0
  232. package/scripts/run/board/run-board-thanks.ts +53 -0
  233. package/scripts/run/board/run-board-tick.ts +50 -21
  234. package/scripts/run/board/run-board-usual.ts +44 -0
  235. package/scripts/run/carry/run-carry-args.ts +9 -1
  236. package/scripts/run/carry/run-carry-cli.ts +2 -45
  237. package/scripts/run/carry/run-carry-end.ts +73 -0
  238. package/scripts/run/carry/run-carry.ts +8 -1
  239. package/scripts/run/entry/run-entry-cli.ts +5 -0
  240. package/scripts/run/event/run-event-checks.ts +54 -0
  241. package/scripts/run/event/run-event-render.ts +2 -0
  242. package/scripts/run/event/run-event-scope.ts +1 -0
  243. package/scripts/run/event/run-event-setup.ts +55 -11
  244. package/scripts/run/event/run-event-stream.ts +27 -11
  245. package/scripts/run/merge/run-merge-cli.ts +7 -4
  246. package/scripts/run/merge/run-merge-ship.ts +61 -0
  247. package/scripts/run/merge/run-merge-steps.ts +7 -14
  248. package/scripts/run/merge/run-merge-token.ts +16 -2
  249. package/scripts/run/run-headless.ts +1 -0
  250. package/scripts/run/run-step.ts +2 -0
  251. package/scripts/run/ship/run-ship-cli.ts +22 -2
  252. package/scripts/run/ship/run-ship-return.ts +31 -13
  253. package/scripts/run/ship/run-ship-review-steps.ts +17 -4
  254. package/scripts/run/ship/run-ship-stage.ts +1 -0
  255. package/scripts/run/wake/run-wake-await.ts +130 -0
  256. package/scripts/run/wake/run-wake-cli.ts +60 -71
  257. package/scripts/run/wake/run-wake-describe.ts +34 -2
  258. package/scripts/run/wake/run-wake-driver.ts +11 -4
  259. package/scripts/run/wake/run-wake-session.ts +20 -3
  260. package/scripts/run/wake/run-wake-stop.ts +37 -0
  261. package/scripts/run/wake/run-wake-supervise.ts +44 -0
  262. package/scripts/sync/claude-settings-merge.ts +144 -0
  263. package/scripts/sync/sync-ai-files.ts +28 -3
  264. package/scripts/sync/sync.ts +15 -0
  265. package/scripts/test/project-test-script.ts +73 -0
  266. package/scripts/test/skip-marker.ts +14 -1
  267. package/scripts/test/test-related.ts +3 -2
  268. package/scripts/test/test-unit-guard.ts +78 -14
  269. package/scripts/time-runtime/time-reported-failure.ts +11 -3
  270. package/scripts/uninstall/distributed-files.ts +92 -0
  271. package/scripts/uninstall/uninstall-plan.ts +119 -0
  272. package/scripts/uninstall/uninstall-report.ts +61 -0
  273. package/scripts/uninstall/uninstall.ts +206 -0
  274. package/templates/AGENTS.basic.md +4 -0
  275. package/templates/GEMINI.basic.md +6 -0
  276. package/dist/hooks/chunk-6V5OPIUU.js +0 -13
  277. package/scripts/lane/openai-review-broker.ts +0 -262
  278. package/scripts/run/board/run-board-track.ts +0 -22
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "kit-test-backlogrun",
3
+ "requires_github": true,
4
+ "package_source": "tarball",
5
+ "requires_claude": true,
6
+ "files": {
7
+ "index.html": "<!doctype html><html><body><h1>Hello</h1></body></html>\n"
8
+ },
9
+ "commands": [
10
+ "gh auth setup-git",
11
+ "git config --global user.name kit-test && git config --global user.email kit-test@example.com",
12
+ "printf '{ \"private\": true }\\n' > package.json",
13
+ "cp \"$PACKAGE_TARBALL\" . && pnpm add -D --allow-build=esbuild --allow-build=unrs-resolver \"./$(basename \"$PACKAGE_TARBALL\")\"",
14
+ "pnpm exec josh start --yes --github",
15
+ "gh label list --limit 100 --json name --jq '.[].name' > \"$HOME/labels.txt\"; grep -xE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\"; test \"$(grep -cxE 'auto-ok|needs-decision|needs-human-review|already-done|review-round2-skipped|bug' \"$HOME/labels.txt\")\" -eq 6",
16
+ "curl -fsSL https://claude.ai/install.sh | bash && export PATH=\"$HOME/.local/bin:$PATH\" && claude --version",
17
+ "gh issue create --title 'Add a heading to index.html' --body 'Add an h2 under the h1.' --label auto-ok --label run:lane",
18
+ "timeout 60 pnpm josh backlogrun > \"$HOME/backlogrun.out\" 2>&1; cat \"$HOME/backlogrun.out\"; grep -q '^backlogrun: started' \"$HOME/backlogrun.out\"",
19
+ "pnpm josh run:event --from 0 | grep 'starting session'",
20
+ "for i in $(seq 1 90); do pnpm josh run:event --from 0 | grep -q 'dispatching' && break; sleep 10; done; pnpm josh run:event --from 0 | grep 'dispatching'",
21
+ "for i in $(seq 1 120); do test \"$(gh pr list --state all --json number --jq length)\" -gt 0 && break; sleep 10; done; pnpm josh run:board --chat",
22
+ "pnpm josh backlogrun --stop"
23
+ ],
24
+ "probes": [
25
+ "tail -n 40 \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\" | cut -c1-400",
26
+ "grep -c 'requires approval' \"$(sed -n 's/^backlogrun: started.*; log //p' \"$HOME/backlogrun.out\")\"",
27
+ "tail -n 40 /tmp/josh-lane-dispatch-*.log | cut -c1-400",
28
+ "cat /tmp/josh-lane-dispatch-*.log | grep -c 'requires approval'",
29
+ "gh pr list --state all"
30
+ ]
31
+ }
@@ -2,6 +2,7 @@ import { GATE_COMMAND } from '#scripts/josh/josh-command-types'
2
2
  import { josh_verdict } from '#scripts/josh/josh-verdict'
3
3
  import { buffered_process, type BufferedProcessResult } from '#scripts/lib/buffered-process'
4
4
  import { status_icons } from '#scripts/lib/status-icons'
5
+ import { NOT_USED_MARKER, NOTHING_TO_CHECK_MARKER } from '#scripts/test/skip-marker'
5
6
  import { test_unit_guard } from '#scripts/test/test-unit-guard'
6
7
  import { gate_log } from './gate-log'
7
8
  import { gate_plan } from './gate-plan'
@@ -11,7 +12,7 @@ import { gate_plan } from './gate-plan'
11
12
  // about what is printed changes in the move: the four blocks, the log notice and the verdict come out
12
13
  // exactly as before, with one addition — the per-failure action lines below.
13
14
 
14
- const { FAIL_ICON, PASS_ICON } = status_icons
15
+ const { FAIL_ICON, PASS_ICON, WARN_ICON } = status_icons
15
16
 
16
17
  interface GateStep {
17
18
  label: string
@@ -76,16 +77,56 @@ function has_warnings(result: GateStepResult): boolean {
76
77
  // other line still withholds.
77
78
  const SAFE_CHAIN_BANNER = 'safe-chain is not available'
78
79
 
80
+ function checker_lines(result: GateStepResult): ReadonlyArray<string> {
81
+ return result.output.split('\n').filter((line) => !line.includes(SAFE_CHAIN_BANNER))
82
+ }
83
+
79
84
  function has_checker_warning(result: GateStepResult): boolean {
80
85
  if (!gate_plan.WARNING_CHECKER_LABELS.includes(result.label)) return false
81
86
 
82
- return result.output
83
- .split('\n')
84
- .some(
85
- (line) =>
86
- !line.includes(SAFE_CHAIN_BANNER) &&
87
- WARNING_MARKERS.some((marker) => line.includes(marker)),
88
- )
87
+ return checker_lines(result).some((line) =>
88
+ WARNING_MARKERS.some((marker) => line.includes(marker)),
89
+ )
90
+ }
91
+
92
+ // The record's counterpart to `is_skip_notice`, narrower for the reason `has_checker_warning` is
93
+ // narrower than `has_warnings`: a skip of a tool the project does not use (`NOT_USED_MARKER`) left
94
+ // nothing unverified, so only another skip line withholds the green record.
95
+ function is_unverified_skip(result: GateStepResult): boolean {
96
+ return checker_lines(result).some(
97
+ (line) => line.includes(test_unit_guard.SKIP_MARKER) && !line.includes(NOT_USED_MARKER),
98
+ )
99
+ }
100
+
101
+ // **A check the verdict counts as skipped said nothing but that it checked nothing.** `is_skip_notice`
102
+ // asks whether a check carries a skip notice, which is the right question for printing its body; it is
103
+ // the wrong one for a count, twice over. A check can skip half of
104
+ // itself — a `basic` project's `lint` runs Prettier and skips eslint — and calling it skipped would
105
+ // have the verdict say nothing ran on a gate that checked the formatting. And a check can run with
106
+ // nothing of the project's to look at — `exports` in a consumer, `behavior` with no transcript — which
107
+ // is no skip, and still left an empty `basic` project reading `2 ran`. A line that is neither notice is
108
+ // something a tool printed, so the check ran. A tool that ran clean and silent beside a skipped one is
109
+ // given a line of its own by the check (`project_checks.ran_notice`), so the reading holds for it too.
110
+ //
111
+ // **pnpm's script header is not something the check printed.** Every check is spawned as
112
+ // `pnpm josh <target>`, and where the project declares a `josh` script — `josh init` writes one — pnpm
113
+ // opens the captured output with `$ josh <target>`. Counted as a line, it made every spawned check one
114
+ // that ran, and `not verified` unreachable in the very project it was written for.
115
+ const IDLE_MARKERS: ReadonlyArray<string> = [test_unit_guard.SKIP_MARKER, NOTHING_TO_CHECK_MARKER]
116
+ const PNPM_SCRIPT_HEADER = '$ '
117
+
118
+ function is_idle_line(line: string): boolean {
119
+ return IDLE_MARKERS.some((marker) => line.includes(marker))
120
+ }
121
+
122
+ function is_tool_line(line: string): boolean {
123
+ return line.trim() !== '' && !line.startsWith(PNPM_SCRIPT_HEADER)
124
+ }
125
+
126
+ function is_idle(result: GateStepResult): boolean {
127
+ const lines = checker_lines(result).filter((line) => is_tool_line(line))
128
+
129
+ return lines.length > 0 && lines.every((line) => is_idle_line(line))
89
130
  }
90
131
 
91
132
  function should_print_body(result: GateStepResult, is_verbose: boolean): boolean {
@@ -144,10 +185,7 @@ function format_failure_actions(failed: ReadonlyArray<GateStepResult>): string {
144
185
 
145
186
  // **A check that started past the core budget is said in the summary, not only in the log**.
146
187
  // The wait cap admits a check that waited two minutes whatever the budget says,
147
- // so a gate that ran over-subscribed would otherwise look exactly like one that fitted. The icon is
148
- // local rather than in `status-icons.ts` because nothing reads this line back.
149
- const WARN_ICON = '⚠'
150
-
188
+ // so a gate that ran over-subscribed would otherwise look exactly like one that fitted.
151
189
  function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
152
190
  const lines = results.flatMap((result) =>
153
191
  result.budget_note === undefined
@@ -158,6 +196,24 @@ function format_budget_notes(results: ReadonlyArray<GateStepResult>): string {
158
196
  return lines.length === 0 ? '' : `${lines.join('\n')}\n`
159
197
  }
160
198
 
199
+ // **The verdict counts what ran apart from what was skipped, and a gate that ran nothing does not say
200
+ // `passed`.** It printed the planned number of checks whatever happened to them, so a `basic` project
201
+ // whose every check skipped read `passed (6 checks)` — the same last line as a gate that ran them all,
202
+ // and the skip this module keeps visible in the bodies was invisible in the one line a `tail` keeps.
203
+ //
204
+ // **A gate with no skip keeps the line it always printed.** A check with nothing to check is counted
205
+ // apart only beside a skip: `exports` says so in every consumer, and counting it alone would reword the
206
+ // verdict of every gate that skipped nothing.
207
+ function format_green_verdict(results: ReadonlyArray<GateStepResult>, total: string): string {
208
+ const has_skip = results.some((result) => is_skip_notice(result))
209
+ const skipped = has_skip ? results.filter((result) => is_idle(result)).length : 0
210
+ const ran = results.length - skipped
211
+
212
+ if (ran === 0) return josh_verdict.format_gate_unverified(skipped, total)
213
+
214
+ return josh_verdict.format_gate_passed({ ran, skipped }, total)
215
+ }
216
+
161
217
  // The total is wall-clock for the whole command, not the sum of the four — they run concurrently,
162
218
  // so a sum would report about three times what the caller waited.
163
219
  //
@@ -172,7 +228,7 @@ function print_gate_summary(
172
228
  const total = format_seconds(report.elapsed_ms)
173
229
  const verdict =
174
230
  failed.length === 0
175
- ? josh_verdict.format_gate_passed(report.step_count, total)
231
+ ? format_green_verdict(results, total)
176
232
  : josh_verdict.format_gate_failed(failed.map((result) => result.label).join(', '), total)
177
233
  const notes = format_budget_notes(results)
178
234
 
@@ -185,7 +241,6 @@ interface GateReport {
185
241
  is_verbose: boolean
186
242
  log_path: string | undefined
187
243
  elapsed_ms: number
188
- step_count: string
189
244
  }
190
245
 
191
246
  // Everything a finished run says, in the order a reader meets it: the four blocks, then the log the
@@ -213,7 +268,7 @@ const gate_report = {
213
268
  format_failure_actions,
214
269
  has_checker_warning,
215
270
  is_gate_step_failed,
216
- is_skip_notice,
271
+ is_unverified_skip,
217
272
  report_gate_steps,
218
273
  }
219
274
 
@@ -2,10 +2,11 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { find_local_bin_upwards } from '#scripts/build/local-bin'
4
4
  import { doctor_consumer } from '#scripts/doctor/doctor-consumer'
5
+ import { own_formatter } from '#scripts/init/own-formatter'
5
6
  import { project_profile } from '#scripts/init/project-profile'
6
7
  import { ancestor_directories } from '#scripts/lib/ancestor-directories'
7
8
  import { json_value } from '#scripts/lib/json-value'
8
- import { SKIP_MARKER } from '#scripts/test/skip-marker'
9
+ import { NOT_USED_MARKER, SKIP_MARKER } from '#scripts/test/skip-marker'
9
10
  import { z } from 'zod'
10
11
 
11
12
  const PACKAGE_JSON = 'package.json'
@@ -126,8 +127,17 @@ function missing_tool_reason(
126
127
  return `${package_name} is not installed`
127
128
  }
128
129
 
130
+ // Every skip reason here names a tool the project does not use — undeclared, or with no files or no
131
+ // configuration to read — so the notice says so (`NOT_USED_MARKER`).
129
132
  function skip_notice(check: string, reason: string): string {
130
- return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}.`
133
+ return `josh ${check}: ${reason} ${SKIP_MARKER} ${check}, ${NOT_USED_MARKER}.`
134
+ }
135
+
136
+ // **A tool that ran clean beside a skipped one says that it ran.** A clean eslint prints nothing, so a
137
+ // `lint` whose Prettier was skipped read as nothing but a skip notice — the same output as a `lint`
138
+ // that ran neither — and the gate's verdict counted a check that linted the whole tree as skipped.
139
+ function ran_notice(check: string): string {
140
+ return `josh ${check}: ran and reported no problems.`
131
141
  }
132
142
 
133
143
  // Shared by `josh check` and the gate's type-check step, so neither reaches `tsc` on a basic
@@ -141,10 +151,12 @@ function type_check_skip_reason(directory: string): string | undefined {
141
151
  }
142
152
 
143
153
  // Shared by `josh lint`, `josh lint:related` and `josh format`, so a basic project without Web
144
- // files — a Python or Rust project, which `josh init` gives no Prettier — is skipped for the same
145
- // reason by all three.
154
+ // files — a Python or Rust project, which `josh init` gives no Prettier — or with a formatter of its
155
+ // own is skipped for the same reason by all three.
146
156
  function prettier_skip_reason(directory: string): string | undefined {
147
157
  if (!is_basic(directory)) return undefined
158
+ const formatter = own_formatter.find_config(project_root(directory))
159
+ if (formatter !== undefined) return `${formatter} configures the project's own formatter`
148
160
  if (!has_files(directory, WEB_FILES)) return 'no HTML, CSS or JavaScript files were found'
149
161
 
150
162
  return missing_tool_reason(directory, 'prettier', 'prettier')
@@ -173,6 +185,8 @@ const project_checks = {
173
185
  is_kit_repository,
174
186
  prettier_skip_reason,
175
187
  project_root,
188
+ ran_notice,
189
+ read_manifest,
176
190
  skip_notice,
177
191
  type_check_skip_reason,
178
192
  }
@@ -64,7 +64,8 @@ interface ScopedSources {
64
64
  }
65
65
 
66
66
  // **`test:related` is required only where it can ever answer**. In a project
67
- // whose unit guard skips — vitest absent, or a basic project with no test file — `test:related` runs
67
+ // whose unit guard skips — vitest absent with no project test script to run in its place, or a basic
68
+ // project with no test file — `test:related` runs
68
69
  // nothing and so writes no record, and requiring one refused the gate forever with no way through.
69
70
  // The guard's own decision is asked rather than re-derived, so the writer and this reader agree; a
70
71
  // project with a unit suite still needs the record, and a run that executed nothing still writes none.
@@ -63,7 +63,9 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
63
63
  // passed *without running* — `test-unit-guard` exits 0 with a notice when vitest is absent or the
64
64
  // project has no tests — must not become "the unit tests all passed": the gate keeps that skip
65
65
  // visible on the console, and a record erasing it would have the brief tell a review agent not to
66
- // re-run tests that never ran. A step that passed **with a checker's warnings** is withheld for the
66
+ // re-run tests that never ran. A skip of a tool the project does not use (`NOT_USED_MARKER` — eslint
67
+ // with no ESLint configuration) is not one: nothing went unverified, so it withholds nothing
68
+ // (`gate_report.is_unverified_skip`). A step that passed **with a checker's warnings** is withheld for the
67
69
  // same reason one place further on: since the record is reused instead of the checks being re-run, a
68
70
  // warning printed once would never be printed again on that tree, and hiding an eslint or
69
71
  // svelte-check finding is the same failure as hiding a skip. It is `has_checker_warning`, not
@@ -80,7 +82,7 @@ async function run_gate_step(step: GateStep): Promise<GateStepResult> {
80
82
  // The destination is a parameter so a test can exercise the record without overwriting the one a
81
83
  // real run may be relying on — `josh gate` and `josh review:brief` share one path by design.
82
84
  function has_nothing_to_say(result: GateStepResult): boolean {
83
- return !gate_report.is_skip_notice(result) && !gate_report.has_checker_warning(result)
85
+ return !gate_report.is_unverified_skip(result) && !gate_report.has_checker_warning(result)
84
86
  }
85
87
 
86
88
  async function record_green_gate(
@@ -362,7 +364,6 @@ async function settle_gate(
362
364
  is_verbose: options.is_verbose ?? false,
363
365
  log_path: options.log_path,
364
366
  elapsed_ms,
365
- step_count: String(plan.checks.length),
366
367
  })
367
368
  const is_passed = failed_labels.length === 0
368
369
 
@@ -0,0 +1,14 @@
1
+ // Whose words kit acts on: the `author_association` REST reports with an issue or a comment, and the
2
+ // ones that can write to the repository. On a public repository anyone can file an issue or comment,
3
+ // so a body kit runs (`josh measure:rerun`) or copies into the ledger (`review:record`) is taken only
4
+ // from an author with write access — and an unknown author is not a trusted one.
5
+ const TRUSTED_ASSOCIATIONS: ReadonlySet<string> = new Set(['OWNER', 'MEMBER', 'COLLABORATOR'])
6
+ const TRUSTED_LIST = [...TRUSTED_ASSOCIATIONS].join(' / ')
7
+
8
+ function is_trusted(association: string | null | undefined): boolean {
9
+ return typeof association === 'string' && TRUSTED_ASSOCIATIONS.has(association)
10
+ }
11
+
12
+ const gh_author_association = { TRUSTED_LIST, is_trusted }
13
+
14
+ export { gh_author_association }
@@ -0,0 +1,66 @@
1
+ import { git_gh_exec, has_stdout_field } from './git-gh-exec'
2
+
3
+ // A `gh api` GET that sends the ETag of the previous answer, so a resource polled every few minutes
4
+ // costs nothing when it has not changed: GitHub answers `304 Not Modified` to a matching
5
+ // `If-None-Match`, and a conditional request answered 304 does not count against the rate limit.
6
+ //
7
+ // **`gh` exits non-zero on a 304** (it prints `gh: HTTP 304`), so the "unchanged" answer arrives as
8
+ // a thrown failure. The status is read off the `--include` status line the failure still carries in
9
+ // its stdout — the same reading `exec_gh_api_status` makes — never off the stderr wording.
10
+
11
+ const NOT_MODIFIED_STATUS = 304
12
+ const OK_STATUS = 200
13
+ const INCLUDE_FLAG = '--include'
14
+ const HEADER_FLAG = '-H'
15
+ const IF_NONE_MATCH = 'If-None-Match: '
16
+ const HEADER_BODY_SEPARATOR = /\r?\n\r?\n/u
17
+ const ETAG_HEADER = /^etag: *(?<etag>[^\r\n]+)/imu
18
+
19
+ type ConditionalRead =
20
+ | { kind: 'unchanged' }
21
+ | { kind: 'changed'; etag: string | undefined; body: string }
22
+ | { kind: 'unreadable' }
23
+
24
+ const UNCHANGED: ConditionalRead = { kind: 'unchanged' }
25
+ const UNREADABLE: ConditionalRead = { kind: 'unreadable' }
26
+
27
+ function to_args(path: string, etag: string | undefined): Array<string> {
28
+ const header = etag === undefined ? [] : [HEADER_FLAG, `${IF_NONE_MATCH}${etag}`]
29
+
30
+ return ['api', INCLUDE_FLAG, ...header, path]
31
+ }
32
+
33
+ // The headers and the body of an `--include` answer, split at the first blank line.
34
+ function to_changed(output: string): ConditionalRead {
35
+ const [headers = '', ...rest] = output.split(HEADER_BODY_SEPARATOR)
36
+ const etag = ETAG_HEADER.exec(headers)?.groups?.['etag']
37
+
38
+ return { kind: 'changed', etag, body: rest.join('\n\n') }
39
+ }
40
+
41
+ function to_read(output: string): ConditionalRead {
42
+ return git_gh_exec.parse_status_line(output) === OK_STATUS ? to_changed(output) : UNREADABLE
43
+ }
44
+
45
+ function failure_status(error: unknown): number | undefined {
46
+ const cause = error instanceof Error ? error.cause : undefined
47
+
48
+ return has_stdout_field(cause) ? git_gh_exec.parse_status_line(cause.stdout) : undefined
49
+ }
50
+
51
+ async function read_conditional(
52
+ path: string,
53
+ etag: string | undefined,
54
+ timeout_ms?: number,
55
+ ): Promise<ConditionalRead> {
56
+ try {
57
+ return to_read(await git_gh_exec.exec_gh_command(to_args(path, etag), timeout_ms))
58
+ } catch (error) {
59
+ return failure_status(error) === NOT_MODIFIED_STATUS ? UNCHANGED : UNREADABLE
60
+ }
61
+ }
62
+
63
+ const git_gh_conditional = { read_conditional, to_args }
64
+
65
+ export type { ConditionalRead }
66
+ export { git_gh_conditional }
@@ -288,7 +288,7 @@ interface GhApiRequest {
288
288
  // second argument for the reason recorded beside that constant. `0` is
289
289
  // execa's own "no budget", kept for the one reader whose caller's writes depend on the answer
290
290
  // (`gh-spawn.ts`).
291
- timeout_ms?: number
291
+ timeout_ms?: number | undefined
292
292
  // The checkout gh runs in, which is what `{owner}/{repo}` in `path` resolves against. Absent means
293
293
  // the process's own directory.
294
294
  cwd?: string
@@ -350,11 +350,14 @@ async function issue_get_labels_and_body(issue_number: string): Promise<string |
350
350
  // No `repo` parameter, unlike the reads above: the auto-close lists its candidate epics from the
351
351
  // repository it runs in, so the epic whose comments are read is always a local one. The path builder
352
352
  // takes one when a caller that needs it appears.
353
- async function issue_list_comments(issue_number: string): Promise<string | undefined> {
353
+ async function issue_list_comments(
354
+ issue_number: string,
355
+ timeout_ms?: number,
356
+ ): Promise<string | undefined> {
354
357
  const path = `${git_gh_api_path.issue_comments_api_path(issue_number)}${git_gh_api_path.FULL_PAGE_QUERY}`
355
358
 
356
359
  try {
357
- return await git_gh_exec.exec_gh_api({ path, should_paginate: true })
360
+ return await git_gh_exec.exec_gh_api({ path, should_paginate: true, timeout_ms })
358
361
  } catch {
359
362
  return undefined
360
363
  }
@@ -436,5 +439,5 @@ const git_gh_issue_read = {
436
439
  issue_list_comments,
437
440
  }
438
441
 
439
- export type { IssueRead, IssueReadFailure }
442
+ export type { IssueRead, IssueReadFailure, IssueReadReason }
440
443
  export { git_gh_issue_read, read_blocked_by, NOT_FOUND_STATUS }
@@ -71,6 +71,7 @@ const REST_FIELD_NAMES = new Map<string, string>([
71
71
  ['url', 'html_url'],
72
72
  ['createdAt', 'created_at'],
73
73
  ['closedAt', 'closed_at'],
74
+ ['updatedAt', 'updated_at'],
74
75
  ])
75
76
 
76
77
  const NOT_AN_ISSUE_MESSAGE = 'gh api answered something other than an issue object'
@@ -1,3 +1,4 @@
1
+ import { issue_watch_diff } from '#scripts/issue/issue-watch-diff'
1
2
  import { session_cite } from '#scripts/issue/session-cite'
2
3
  import { error_text } from '#scripts/lib/error-message'
3
4
  import { git_gh_api_path } from './git-gh-api-path'
@@ -64,10 +65,13 @@ async function issue_edit_body(issue_number: string, body: string): Promise<stri
64
65
  })
65
66
  }
66
67
 
68
+ // **Every comment josh posts carries `RUN_MARKER`**, an HTML comment GitHub does not render. A run
69
+ // posts under the person's own `gh` login, so the marker is the only thing that tells the issue watch
70
+ // (`issue-watch-diff.ts`) a run's comment from a person's update.
67
71
  async function issue_comment(issue_number: string, body: string): Promise<string> {
68
72
  return await git_gh_exec.exec_gh_api({
69
73
  path: `${git_gh_api_path.issue_api_path(issue_number)}${COMMENTS_SEGMENT}`,
70
- body: JSON.stringify({ body }),
74
+ body: JSON.stringify({ body: `${body}\n\n${issue_watch_diff.RUN_MARKER}` }),
71
75
  jq_filter: HTML_URL_FILTER,
72
76
  })
73
77
  }
@@ -14,7 +14,10 @@ const NUMBER_AND_BODY_FIELDS = 'number,body'
14
14
  // tell a row with no blockers from one with some **before** deciding whether that row is worth a
15
15
  // per-issue read, which would otherwise be made for every open issue in the repository. Absent on a
16
16
  // row whose summary is missing, which must not be read as zero; `git-gh-issue-rest.ts` states why.
17
- const BACKLOG_SCAN_FIELDS = `number,title,body,${BLOCKED_BY_COUNT_FIELD}`
17
+ //
18
+ // `labels` rides along on the same response too: `epic:bundle` reads the release label off it,
19
+ // because the release issue's blockers are recorded mechanically and signal no shared work.
20
+ const BACKLOG_SCAN_FIELDS = `number,title,body,labels,${BLOCKED_BY_COUNT_FIELD}`
18
21
  // The recently-closed scan's fields. It compares titles like the scan above
19
22
  // and never reads a body, and it needs the labels to tell an epic from a deliverable — the open half
20
23
  // gets that from the epic listing, which by construction holds no closed epic.
@@ -147,6 +150,14 @@ async function issue_list_by_label_in_repo(
147
150
  return await issue_list({ json_fields: SUMMARY_FIELDS, limit, label, repo })
148
151
  }
149
152
 
153
+ // Every open issue with what `josh auto-ok:judge` judges it by — its labels and body — and `updatedAt`,
154
+ // so the backlog driver's resident check reads an issue's comments again only once it has moved.
155
+ const JUDGEMENT_FIELDS = 'number,title,body,labels,updatedAt'
156
+
157
+ async function issue_list_open_for_judgement(limit: number): Promise<IssueListOutcome> {
158
+ return await issue_list({ json_fields: JUDGEMENT_FIELDS, limit })
159
+ }
160
+
150
161
  const git_gh_issue = {
151
162
  ...git_gh_issue_read,
152
163
  ...git_gh_issue_write,
@@ -156,6 +167,7 @@ const git_gh_issue = {
156
167
  issue_list_by_label_summary,
157
168
  issue_search_body,
158
169
  issue_list_open_bodies,
170
+ issue_list_open_for_judgement,
159
171
  issue_list_recently_closed,
160
172
  }
161
173
 
@@ -119,6 +119,18 @@ function is_coderabbit_check(check_name: string): boolean {
119
119
  return is_required_match(check_name, CODERABBIT_CHECK_NAME)
120
120
  }
121
121
 
122
+ // CodeRabbit reports a rate-limited review as a *passing* status whose description says so
123
+ // (`Review rate limited`), so only the description tells it from a finished review
124
+ // (joshuafolkken/kit#3770).
125
+ const CODERABBIT_RATE_LIMIT_DESCRIPTION = /rate limit/iu
126
+
127
+ function is_coderabbit_rate_limited(check: RollupCheck): boolean {
128
+ return (
129
+ is_coderabbit_check(check.name) &&
130
+ CODERABBIT_RATE_LIMIT_DESCRIPTION.test(check.description ?? '')
131
+ )
132
+ }
133
+
122
134
  // Temporary: a pending or failing CodeRabbit check makes GitHub report UNSTABLE even
123
135
  // though CodeRabbit is not required. Accept that state only when CodeRabbit checks are the
124
136
  // sole non-passing ones — any other non-passing check keeps the gate strict.
@@ -249,6 +261,7 @@ export {
249
261
  is_review_decision_decisive,
250
262
  read_required_statuses,
251
263
  is_coderabbit_check,
264
+ is_coderabbit_rate_limited,
252
265
  collect_blocking_failures,
253
266
  describe_pr_failure,
254
267
  REQUIRED_CHECKS,
@@ -31,10 +31,14 @@ const KEY_STATUS = 'status'
31
31
  const KEY_CONCLUSION = 'conclusion'
32
32
  const KEY_NAME = 'name'
33
33
  const KEY_CONTEXT = 'context'
34
+ const KEY_DESCRIPTION = 'description'
34
35
 
35
36
  interface RollupCheck {
36
37
  name: string
37
38
  status: string
39
+ // A status context's own words — CodeRabbit says `Review rate limited` under a `success` state
40
+ // (joshuafolkken/kit#3770). Absent where the check carries none.
41
+ description?: string
38
42
  }
39
43
 
40
44
  interface PrStateSnapshot {
@@ -95,7 +99,10 @@ function parse_rollup_item(item: RollupItemData): RollupCheck | undefined {
95
99
  const name = read_string(item[KEY_NAME]) ?? read_string(item[KEY_CONTEXT])
96
100
  if (name === undefined) return undefined
97
101
 
98
- return { name, status: parse_rollup_status(item) }
102
+ const check = { name, status: parse_rollup_status(item) }
103
+ const description = read_string(item[KEY_DESCRIPTION])
104
+
105
+ return description === undefined ? check : { ...check, description }
99
106
  }
100
107
 
101
108
  function parse_rollup_checks(raw_json: string): Array<RollupCheck> {
@@ -4,9 +4,11 @@ import {
4
4
  default_fetch_pr_state,
5
5
  DEFAULT_STABLE_READS,
6
6
  is_pr_checks_timeout,
7
+ observed_fetcher,
7
8
  SECONDS_TO_MS,
8
9
  wait_for_pr_success,
9
10
  type PrStateEvaluator,
11
+ type SnapshotObserver,
10
12
  } from './git-pr-checks'
11
13
  import { is_coderabbit_check, is_required_check, type PrEvaluation } from './git-pr-checks-eval'
12
14
  import {
@@ -125,10 +127,12 @@ const CHECKS_SETTLED_EVALUATOR: PrStateEvaluator = {
125
127
  should_read_review_decision: should_never_read_review_decision,
126
128
  }
127
129
 
128
- async function watch_until_settled(branch_name: string): Promise<void> {
130
+ // `observe` sees the watch's polls too, so a red wait's checks line is replaced while the new CI is
131
+ // still pending rather than only after the watch (joshuafolkken/kit#3770).
132
+ async function watch_until_settled(branch_name: string, observe?: SnapshotObserver): Promise<void> {
129
133
  await wait_for_pr_success({
130
134
  branch_name,
131
- fetcher: default_fetch_pr_state,
135
+ fetcher: observed_fetcher(observe),
132
136
  interval_ms: CHECK_WAIT_INTERVAL_MS,
133
137
  max_attempts: compute_max_attempts(
134
138
  PR_CHECKS_WATCH_TIMEOUT_MS / SECONDS_TO_MS,
@@ -154,9 +158,12 @@ async function fail_when_no_checks(branch_name: string): Promise<void> {
154
158
  // Everything else — a failing check, an unreadable read, a branch with no checks — is rethrown, which
155
159
  // is what the caller already handles: `git-pr-followup.ts` swallows it and falls through to its own
156
160
  // poll.
157
- async function pr_checks_watch(branch_name: string): Promise<WatchResult> {
161
+ async function pr_checks_watch(
162
+ branch_name: string,
163
+ observe?: SnapshotObserver,
164
+ ): Promise<WatchResult> {
158
165
  try {
159
- await watch_until_settled(branch_name)
166
+ await watch_until_settled(branch_name, observe)
160
167
 
161
168
  return { timed_out: false }
162
169
  } catch (error) {
@@ -128,6 +128,9 @@ interface WaitForPrSuccessOptions {
128
128
  max_attempts: number
129
129
  required_stable_reads: number
130
130
  evaluator?: PrStateEvaluator
131
+ // Asked before every poll; a throw ends the wait with that error. `josh followup` stops here on a
132
+ // person's unread issue update (`issue-watch-gate.ts`).
133
+ before_poll?: (() => Promise<void>) | undefined
131
134
  }
132
135
 
133
136
  function parse_repo_name_from_package(package_json_content: string): string {
@@ -272,6 +275,8 @@ async function wait_for_pr_success(options: WaitForPrSuccessOptions): Promise<Pr
272
275
 
273
276
  for (let attempt = 0; attempt < options.max_attempts; attempt += 1) {
274
277
  console.info(`Checking PR status… (${String(attempt + 1)}/${String(options.max_attempts)})`)
278
+ // eslint-disable-next-line no-await-in-loop -- polling: the check belongs to this poll, not to all of them at once
279
+ await options.before_poll?.()
275
280
  // eslint-disable-next-line no-await-in-loop -- polling: each read waits on the state the previous one saw
276
281
  carried = await attempt_pr_success_poll({
277
282
  options,
@@ -311,16 +316,38 @@ async function default_fetch_pr_state(
311
316
  // **`required_stable_reads` is a parameter, not a constant, so the caller can lower it** — `run_checks`
312
317
  // passes `WATCH_CONFIRMED_STABLE_READS` when the watch already confirmed completion. It defaults to
313
318
  // `DEFAULT_STABLE_READS`, so every caller that names only the branch is exactly where it was.
319
+ // What a caller is shown of each poll it waits on — `followup` reports the checks to the run's event
320
+ // stream from it (joshuafolkken/kit#3770). It decides nothing: the wait reads the same snapshot.
321
+ type SnapshotObserver = (snapshot: PrStateSnapshot) => Promise<void>
322
+
323
+ function observed_fetcher(observe: SnapshotObserver | undefined): PrStateFetcher {
324
+ if (observe === undefined) return default_fetch_pr_state
325
+
326
+ return async function fetch_observed(
327
+ branch_name: string,
328
+ should_read_review_decision: ReviewDecisionPredicate,
329
+ ): Promise<PrStateSnapshot> {
330
+ const snapshot = await default_fetch_pr_state(branch_name, should_read_review_decision)
331
+
332
+ await observe(snapshot)
333
+
334
+ return snapshot
335
+ }
336
+ }
337
+
314
338
  async function wait_for_pr_success_default(
315
339
  branch_name: string,
316
340
  required_stable_reads: number = DEFAULT_STABLE_READS,
341
+ before_poll?: () => Promise<void>,
342
+ observe?: SnapshotObserver,
317
343
  ): Promise<PrStateSnapshot> {
318
344
  return await wait_for_pr_success({
319
345
  branch_name,
320
- fetcher: default_fetch_pr_state,
346
+ fetcher: observed_fetcher(observe),
321
347
  interval_ms: CHECK_WAIT_INTERVAL_MS,
322
348
  max_attempts: CHECK_MAX_ATTEMPTS,
323
349
  required_stable_reads,
350
+ before_poll,
324
351
  })
325
352
  }
326
353
 
@@ -383,10 +410,17 @@ export {
383
410
  SECONDS_TO_MS,
384
411
  is_pr_checks_timeout,
385
412
  default_fetch_pr_state,
413
+ observed_fetcher,
386
414
  MERGE_GATE_EVALUATOR,
387
415
  should_always_read_review_decision,
388
416
  }
389
- export type { MergeProgress, PrStateFetcher, PrStateEvaluator, ReviewDecisionPredicate }
417
+ export type {
418
+ MergeProgress,
419
+ PrStateFetcher,
420
+ PrStateEvaluator,
421
+ ReviewDecisionPredicate,
422
+ SnapshotObserver,
423
+ }
390
424
  export {
391
425
  collect_blocking_failures,
392
426
  describe_pr_failure,