@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
@@ -2,6 +2,7 @@
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { change_base } from '#scripts/git/change-base'
4
4
  import { path_decision } from '#scripts/josh/path-decision'
5
+ import { review_code_tree } from './review-code-tree'
5
6
  import { review_round2 } from './review-round2'
6
7
  import { review_stamps } from './review-stamps'
7
8
  import { review_tree } from './review-tree'
@@ -44,6 +45,14 @@ function parse_options(argv: ReadonlyArray<string>): Options | undefined {
44
45
  }
45
46
  }
46
47
 
48
+ // Only the paths the code record covers are re-read, so a run without one parses nothing.
49
+ async function read_comment_only(base: string | undefined): Promise<ReadonlyArray<string>> {
50
+ const snapshot = review_stamps.round_one_code_stamp.read()
51
+ const tree = await review_code_tree.read(Object.keys(snapshot?.files ?? {}))
52
+
53
+ return review_code_tree.comment_only(snapshot, tree, base)
54
+ }
55
+
47
56
  async function run(argv: ReadonlyArray<string>): Promise<number> {
48
57
  const options = parse_options(argv)
49
58
 
@@ -53,11 +62,13 @@ async function run(argv: ReadonlyArray<string>): Promise<number> {
53
62
  return FAILURE_EXIT_CODE
54
63
  }
55
64
 
65
+ // The commit the tree below is a diff against, so the snapshot can be refused when it was taken
66
+ // against a different one — and the resolved commit rather than `git_command.change_base`, whose
67
+ // fallback is a ref name that moves and would compare equal to itself.
68
+ const base = await change_base.resolved()
56
69
  const { verdict, reason } = review_round2.decide({
57
- // The commit the tree below is a diff against, so the snapshot can be refused when it was taken
58
- // against a different one — and the resolved commit rather than `git_command.change_base`, whose
59
- // fallback is a ref name that moves and would compare equal to itself.
60
- base: await change_base.resolved(),
70
+ base,
71
+ comment_only: await read_comment_only(base),
61
72
  is_round_one_closed: options.is_round_one_closed,
62
73
  snapshot: review_stamps.round_one_stamp.read(),
63
74
  // No paths argument: `read_changed_tree` reads them itself, from the one definition of
@@ -76,7 +87,7 @@ async function main(argv: ReadonlyArray<string>): Promise<void> {
76
87
  process.exitCode = await run(argv)
77
88
  }
78
89
 
79
- const review_round2_cli = { CLOSED_FLAG, parse_options, run }
90
+ const review_round2_cli = { CLOSED_FLAG, parse_options, read_comment_only, run }
80
91
 
81
92
  if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
82
93
 
@@ -9,14 +9,14 @@ import { review_level } from './review-level'
9
9
  //
10
10
  // **The two arms are the two states in which round 2's reason does not arise.** Round 2 exists
11
11
  // because round 1's *fix code* is unreviewed, and that is structural. Arm A applies where there is no fix code — the findings closed without an edit — so
12
- // the premise is absent rather than overridden. Arm B applies where the fix code neither executes,
13
- // nor instructs, nor ships, which is `review-level.ts`'s inert set: the same three ways a defect in
14
- // this repository escapes, read here for the fix delta instead of the whole change.
12
+ // the premise is absent rather than overridden. Arm B applies where the fix leaves what executes
13
+ // unchanged: `review-level.ts`'s inert set, documentation under `docs/` and the root `README.md`,
14
+ // `prompts/`, and source files whose only change is in comments (`review-code-tree.ts`).
15
15
  //
16
- // **The wider line — "the fix delta touches no runtime code path" — is not adopted**: as
17
- // `review-level.ts` records, documentation-only diffs carry real defects no test covers. A prompt
18
- // fix and a test fix are exactly where the review is the only detector, so exempting them would
19
- // remove the round in the case it earns its keep.
16
+ // **Arm B is wider than the review level's inert set, on purpose** (joshuafolkken/kit#3787). Round 1
17
+ // still reviews a documentation or prompt change in full, and the fix to its findings is still made;
18
+ // what goes unread is only that fix. `.claude/**`, `CLAUDE.md`, tests and configuration stay outside:
19
+ // a test fix can weaken the verification it belongs to, and nothing reads that but the review.
20
20
 
21
21
  type RoundTwoVerdict = 'required' | 'skip'
22
22
 
@@ -33,7 +33,11 @@ const NO_SNAPSHOT_REASON =
33
33
  const EMPTY_DELTA_REASON_PREFIX =
34
34
  'the fix delta is empty: round 1 wrote no fix code, so there is no unreviewed fix for a verification pass to read'
35
35
  const INERT_DELTA_REASON_PREFIX =
36
- 'every path round 1 fixed is inert — it neither executes, nor instructs, nor ships'
36
+ 'every path round 1 fixed leaves what executes unchanged — inert, documentation, a prompt, or a comment-only edit'
37
+ const README_PATH = 'README.md'
38
+ const DOCUMENTATION_PREFIX = 'docs/'
39
+ const MARKDOWN_EXTENSION = '.md'
40
+ const PROMPTS_PREFIX = 'prompts/'
37
41
  const BASE_MOVED_REASON =
38
42
  'the change base moved since round 1 was recorded, so the two file maps do not cover the same set of paths and their difference is not the fix delta — the round runs rather than reading that difference as one'
39
43
 
@@ -51,6 +55,9 @@ interface RoundTwoInput {
51
55
  is_round_one_closed: boolean
52
56
  snapshot: FileMapStamp | undefined
53
57
  tree: Record<string, string>
58
+ // The paths whose code round 1's fixes left as it was (`review_code_tree.comment_only`). Absent is
59
+ // none, which is what a run without a code record pays.
60
+ comment_only?: ReadonlyArray<string>
54
61
  }
55
62
 
56
63
  interface RoundTwoDecision {
@@ -86,13 +93,26 @@ function live_reason(deciding: ReadonlyArray<string>): string {
86
93
  return `round 1's fixes touched paths that execute, instruct or ship: ${path_decision.format_path_list(deciding)}`
87
94
  }
88
95
 
96
+ function is_documentation(relative: string): boolean {
97
+ if (relative === README_PATH || relative.startsWith(PROMPTS_PREFIX)) return true
98
+
99
+ return relative.startsWith(DOCUMENTATION_PREFIX) && relative.endsWith(MARKDOWN_EXTENSION)
100
+ }
101
+
102
+ function is_unexecuted(relative: string, comment_only: ReadonlyArray<string>): boolean {
103
+ if (review_level.is_inert(relative) || is_documentation(relative)) return true
104
+
105
+ return comment_only.includes(relative)
106
+ }
107
+
89
108
  function verdict_for_delta(
90
109
  delta: ReadonlyArray<string>,
91
110
  taken_at: string,
111
+ comment_only: ReadonlyArray<string> = [],
92
112
  ): { verdict: RoundTwoVerdict; reason: string } {
93
113
  if (delta.length === 0) return { verdict: SKIPPED_VERDICT, reason: empty_reason(taken_at) }
94
114
 
95
- const deciding = delta.filter((relative) => !review_level.is_inert(relative))
115
+ const deciding = delta.filter((relative) => !is_unexecuted(relative, comment_only))
96
116
 
97
117
  if (deciding.length === 0) return { verdict: SKIPPED_VERDICT, reason: inert_reason(delta) }
98
118
 
@@ -110,7 +130,7 @@ function decide(input: RoundTwoInput): RoundTwoDecision {
110
130
 
111
131
  const delta = file_map_stamp.changed_since(input.snapshot, input.tree)
112
132
 
113
- return { ...verdict_for_delta(delta, input.snapshot.taken_at), delta }
133
+ return { ...verdict_for_delta(delta, input.snapshot.taken_at, input.comment_only), delta }
114
134
  }
115
135
 
116
136
  const review_round2 = {
@@ -13,11 +13,17 @@ const TEST_RELATED_PREFIX = 'josh-test-related-stamp-'
13
13
  const GATE_PREFIX = 'josh-gate-stamp-'
14
14
  const IN_FLIGHT_PREFIX = 'josh-gate-running-'
15
15
  const ROUND_ONE_PREFIX = 'josh-review-round1-'
16
+ const ROUND_ONE_CODE_PREFIX = 'josh-review-round1-code-'
16
17
  const BRIEFED_PREFIX = 'josh-review-briefed-'
17
18
 
18
19
  const gate_stamp: FileMapStampAccess = file_map_stamp.create(GATE_PREFIX, PROJECT_ROOT)
19
20
  const in_flight_stamp: FileMapStampAccess = file_map_stamp.create(IN_FLIGHT_PREFIX, PROJECT_ROOT)
20
21
  const round_one_stamp: FileMapStampAccess = file_map_stamp.create(ROUND_ONE_PREFIX, PROJECT_ROOT)
22
+ // Written beside the round-1 snapshot: the comment-free digest of each source file (`review-code-tree.ts`).
23
+ const round_one_code_stamp: FileMapStampAccess = file_map_stamp.create(
24
+ ROUND_ONE_CODE_PREFIX,
25
+ PROJECT_ROOT,
26
+ )
21
27
  // Retaken on every brief: the round-1 snapshot is not the tree a later review read.
22
28
  const briefed_stamp: FileMapStampAccess = file_map_stamp.create(BRIEFED_PREFIX, PROJECT_ROOT)
23
29
  const lint_related_stamp: FileMapStampAccess = file_map_stamp.create(
@@ -29,24 +35,39 @@ const test_related_stamp: FileMapStampAccess = file_map_stamp.create(
29
35
  PROJECT_ROOT,
30
36
  )
31
37
 
38
+ // Swallowed: a code record left behind only ever sits beside an older snapshot, which the next
39
+ // round-1 brief replaces together with it.
40
+ function clear_round_one_code(target?: string): void {
41
+ try {
42
+ round_one_code_stamp.remove(target)
43
+ } catch {
44
+ /* the next round-1 brief overwrites it */
45
+ }
46
+ }
47
+
32
48
  // Called only from the end of a merged run: a leftover widens the next delta (safe), while an early
33
49
  // removal lets round 1 re-snapshot an already-fixed tree (unsafe). Swallowed — the PR already merged.
34
- function clear_round_one(target?: string): void {
50
+ function clear_round_one(target?: string, code_target?: string): void {
35
51
  try {
36
52
  round_one_stamp.remove(target)
37
53
  } catch {
38
54
  /* a record left behind widens the next round rather than narrowing it */
39
55
  }
56
+
57
+ clear_round_one_code(code_target)
40
58
  }
41
59
 
42
60
  const review_stamps = {
43
61
  briefed_stamp,
44
62
  clear_round_one,
63
+ clear_round_one_code,
45
64
  GATE_PREFIX,
46
65
  gate_stamp,
47
66
  IN_FLIGHT_PREFIX,
48
67
  in_flight_stamp,
49
68
  lint_related_stamp,
69
+ ROUND_ONE_CODE_PREFIX,
70
+ round_one_code_stamp,
50
71
  ROUND_ONE_PREFIX,
51
72
  round_one_stamp,
52
73
  test_related_stamp,
@@ -176,9 +176,9 @@ const IMPLEMENTATION_CUT_REASON =
176
176
  'because the verdict was only ever read at session entry, where the context has not yet grown. The ' +
177
177
  "verdict is `pnpm josh cost --cut`'s exactly — the same per-request billed-input statistic against the " +
178
178
  'same threshold, never a second measurement. Issue `pnpm josh run:cut --impl <N> --handoff <path>` now and read the ' +
179
- 'verdict: on `cut`, **end the turn immediately** — in a lane the fresh process owns the run and ' +
180
- 'continues implementing, so it must not be waited for; outside a lane nothing is relaunched, so first ' +
181
- 'send the `confirmation` Telegram naming the resume command `fullrun #<N>` and keep the hold; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
179
+ 'verdict: on `cut`, read what the command printed. Where it relaunched a fresh process, **end the turn ' +
180
+ 'immediately** — that process owns the run and continues implementing, so it must not be waited for, ' +
181
+ 'and **no Telegram is owed**: a cut that resumes itself leaves a person nothing to act on. Where it printed that nothing was relaunched, do what it prints, then end the turn; on `not-a-lane`, `unready`, `busy`, `failed` or ' +
182
182
  '`unknown`, this process carries the run on and the edit is simply the next call. Never relaunch a ' +
183
183
  'second process after `busy`. This fired because the dispatch mark names this lane or the run hold ' +
184
184
  'names this issue; a person working here carries no mark and holds no run, so sees no refusal, so there is no human-or-child judgement left to make. The ' +
@@ -172,7 +172,7 @@ const PRE_GATE_CUT_REASON =
172
172
  '176K of 204K output riding across that boundary, and joshuafolkken/kit#1864 measured the step ' +
173
173
  'itself firing 0 times in 6 lane children while it was carried as prose. Issue ' +
174
174
  '`pnpm josh run:cut <N>` now, and read the verdict: on `cut`, **end the turn immediately** — the ' +
175
- 'fresh process owns the run from there and must not be waited for; on `not-a-lane`, `unready`, ' +
175
+ 'fresh process owns the run from there and must not be waited for, and **no Telegram is owed** for a cut that resumes itself; on `not-a-lane`, `unready`, ' +
176
176
  '`busy`, `failed` or `unknown`, this process carries the run on and the gate is simply the next ' +
177
177
  'call. Never relaunch a second process after `busy`. This fired because the dispatch mark names ' +
178
178
  'this lane; a person working here carries no mark and sees no refusal, so there is no ' +
@@ -0,0 +1,42 @@
1
+ import { run_event_checks, type PrChecks } from '#scripts/run/event/run-event-checks'
2
+ import { run_event_scope } from '#scripts/run/event/run-event-scope'
3
+ import { run_event_stream, type RunEvent } from '#scripts/run/event/run-event-stream'
4
+
5
+ // Each child's newest `pr-checks` line — its pull request's CI counts and CodeRabbit's state as
6
+ // `josh followup` last polled them (joshuafolkken/kit#3770). Read from the stream alone, so a redraw
7
+ // asks GitHub nothing.
8
+
9
+ interface HeldChecks {
10
+ checks: PrChecks
11
+ at_ms: number
12
+ }
13
+
14
+ const { PR_CHECKS } = run_event_stream.EVENT_KIND
15
+
16
+ function held_of(event: RunEvent): HeldChecks | undefined {
17
+ const checks = event.kind === PR_CHECKS ? run_event_checks.checks_of(event.text) : undefined
18
+
19
+ return checks === undefined ? undefined : { checks, at_ms: Date.parse(event.at) }
20
+ }
21
+
22
+ // The stream's bound reorders its lines, so the newest is the latest `at`, not the last line.
23
+ function is_newer(held: HeldChecks, current: HeldChecks | undefined): boolean {
24
+ return current === undefined || held.at_ms >= current.at_ms
25
+ }
26
+
27
+ function newest_of(events: ReadonlyArray<RunEvent>): ReadonlyMap<number, PrChecks> {
28
+ const newest = new Map<number, HeldChecks>()
29
+
30
+ for (const event of events) {
31
+ const held = held_of(event)
32
+ const issue = Number(run_event_scope.issue_named(event))
33
+
34
+ if (held !== undefined && is_newer(held, newest.get(issue))) newest.set(issue, held)
35
+ }
36
+
37
+ return new Map([...newest].map(([issue, held]) => [issue, held.checks]))
38
+ }
39
+
40
+ const run_board_checks = { newest_of }
41
+
42
+ export { run_board_checks }
@@ -15,6 +15,7 @@ import { run_board_read } from './run-board-read'
15
15
  import { run_board_screen, type Screen } from './run-board-screen'
16
16
  import { run_board_spin } from './run-board-spin'
17
17
  import type { BoardPorts } from './run-board-state'
18
+ import { run_board_thanks_read } from './run-board-thanks-read'
18
19
  import { run_board_tick } from './run-board-tick'
19
20
  import { run_board_usage_read } from './run-board-usage-read'
20
21
 
@@ -52,6 +53,7 @@ const LIVE_PORTS: BoardPorts = {
52
53
  read_machine: machine_capacity.read_sample,
53
54
  read_usage: run_board_usage_read.usage_reader(),
54
55
  read_closed: async (issues) => await run_board_closed.read_all(issues),
56
+ read_thanks: async () => await run_board_thanks_read.read_thanks(),
55
57
  now: () => Date.now(),
56
58
  write: (frame) => process.stdout.write(frame),
57
59
  // No fallback: a terminal that opens no link, or a pipe, gets the bare number rather than a URL.
@@ -15,7 +15,8 @@ interface ChatFrame {
15
15
  local: LocalRead | undefined
16
16
  }
17
17
 
18
- // One frame in the chat's form, with every escape stripped because a chat shows them as text.
18
+ // One frame in the chat's form, with every escape stripped because a chat shows them as text. The
19
+ // reviewers are thanked on the terminal's board alone, so the chat reads no roster.
19
20
  async function chat_frame(ports: BoardPorts): Promise<ChatFrame> {
20
21
  let text = ''
21
22
  const chat_ports: BoardPorts = {
@@ -23,6 +24,7 @@ async function chat_frame(ports: BoardPorts): Promise<ChatFrame> {
23
24
  is_tty: false,
24
25
  size: undefined,
25
26
  form: 'chat',
27
+ read_thanks: undefined,
26
28
  write: (frame) => {
27
29
  text += stripVTControlCharacters(frame)
28
30
  },
@@ -5,14 +5,17 @@ import { run_board_labels } from './run-board-labels'
5
5
  // A live `run:board` frame kept within the terminal. A frame taller than the pane scrolls the
6
6
  // alternate screen as it is written, and the header — the lines that say whether the run moves and
7
7
  // how far it is — is what scrolls off. So the parts below it give way instead, least wanted first:
8
- // the footer, the legend, the finished rows, the notes, the rows not yet started, the parked rows
9
- // and, last, the running rows — what a person keeps the board open to see. Each stage stops once the
8
+ // the footer, the legend, the oldest notes until three are left, the finished rows, the rest of the
9
+ // notes, the reviewers thanked above the legend, the rows not yet started, the parked rows and, last,
10
+ // the running rows — what a person keeps the board open to see. Each stage stops once the
10
11
  // frame fits, a cut row is counted on its section's `more N` line, and the order a row is drawn in
11
12
  // never changes. A line wider than the pane wraps, so every line is counted by the rows it takes on
12
13
  // screen, not as one.
13
14
 
14
15
  const { WORDS } = run_board_labels
15
16
  const INDENT = ' '
17
+ // The newest notes kept until the finished rows are gone; the older ones give way before them.
18
+ const NOTES_KEPT = 3
16
19
 
17
20
  interface TerminalSize {
18
21
  rows: number
@@ -52,21 +55,24 @@ interface FrameParts {
52
55
  above: ReadonlyArray<string>
53
56
  plan: ReadonlyArray<PlanSection>
54
57
  notes: NotesPart
58
+ // The reviewers thanked, drawn right above the legend; none where nobody is.
59
+ thanks?: ReadonlyArray<string> | undefined
55
60
  legend: ReadonlyArray<string>
56
61
  footer: ReadonlyArray<string>
57
62
  }
58
63
 
59
64
  // One thing that gives way: a part as a whole, one note (the oldest shown), or one issue row.
60
- type Yielder = 'footer' | 'legend' | 'note' | PlanLine
65
+ type Yielder = 'thanks' | 'footer' | 'legend' | 'note' | PlanLine
61
66
 
62
67
  interface Cut {
68
+ thanks: boolean
63
69
  footer: boolean
64
70
  legend: boolean
65
71
  notes: number
66
72
  rows: ReadonlySet<PlanLine>
67
73
  }
68
74
 
69
- const NO_CUT: Cut = { footer: false, legend: false, notes: 0, rows: new Set() }
75
+ const NO_CUT: Cut = { thanks: false, footer: false, legend: false, notes: 0, rows: new Set() }
70
76
 
71
77
  // The columns a line takes on screen: its color escapes take none, an emoji two.
72
78
  function width_of(line: string): number {
@@ -117,6 +123,7 @@ function compose(parts: FrameParts, cut: Cut): Array<string> {
117
123
  ...parts.above,
118
124
  ...plan_lines(parts.plan, cut.rows),
119
125
  ...notes_lines(parts.notes, cut.notes),
126
+ ...(cut.thanks ? [] : (parts.thanks ?? [])),
120
127
  ...(cut.legend ? [] : parts.legend),
121
128
  ...(cut.footer ? [] : parts.footer),
122
129
  ]
@@ -154,12 +161,15 @@ function order_of(parts: FrameParts): Array<Yielder> {
154
161
  .map((line, index) => ({ line, index }))
155
162
  .filter((entry) => entry.line.row !== undefined)
156
163
  const notes = parts.notes.lines.map((): Yielder => 'note')
164
+ const surplus = Math.max(0, notes.length - NOTES_KEPT)
157
165
 
158
166
  return [
159
167
  'footer',
160
168
  'legend',
169
+ ...notes.slice(0, surplus),
161
170
  ...yielding(rows, 'finished'),
162
- ...notes,
171
+ ...notes.slice(surplus),
172
+ 'thanks',
163
173
  ...yielding(rows, 'pending'),
164
174
  ...yielding(rows, 'parked'),
165
175
  ...yielding(rows, 'running'),
@@ -175,6 +185,7 @@ function cut_of(order: ReadonlyArray<Yielder>, count: number): Cut {
175
185
  const gone = order.slice(0, count)
176
186
 
177
187
  return {
188
+ thanks: gone.includes('thanks'),
178
189
  footer: gone.includes('footer'),
179
190
  legend: gone.includes('legend'),
180
191
  notes: gone.filter((item) => item === 'note').length,
@@ -0,0 +1,64 @@
1
+ import { styleText } from 'node:util'
2
+ import { run_progress } from '#scripts/run/progress/run-progress'
3
+
4
+ // **The one place a freshly updated part of `run:board` is emphasized** — the header's `💓` and third
5
+ // line, an issue row's phase, start and end (joshuafolkken/kit#3784). The emphasis fades in three
6
+ // one-second stages, decided by the part's age alone, so no frame remembers the one before it. Only a
7
+ // live screen draws it: a chat, a `--once` frame or a pipe gets the text as it is.
8
+
9
+ type FreshStage = 'inverse' | 'bold-color' | 'bold' | 'none'
10
+
11
+ type TextFormat = Parameters<typeof styleText>[0]
12
+
13
+ // What decides whether a frame emphasizes at all: a screen frame with a spinner is a live terminal.
14
+ interface FreshFrame {
15
+ now_ms: number
16
+ form: 'screen' | 'chat'
17
+ spinner: string | undefined
18
+ }
19
+
20
+ const STAGE_MS = run_progress.MS_PER_SECOND
21
+ const FADING: ReadonlyArray<FreshStage> = ['inverse', 'bold-color', 'bold']
22
+ const FORMATS: Readonly<Record<Exclude<FreshStage, 'none'>, TextFormat>> = {
23
+ inverse: 'inverse',
24
+ 'bold-color': ['bold', 'cyan'],
25
+ bold: 'bold',
26
+ }
27
+
28
+ // The stage a part `age_ms` old is drawn in; an unknown or a future age is never emphasized.
29
+ function stage_of(age_ms: number | undefined): FreshStage {
30
+ if (age_ms === undefined || age_ms < 0) return 'none'
31
+
32
+ return FADING[Math.floor(age_ms / STAGE_MS)] ?? 'none'
33
+ }
34
+
35
+ function paint(text: string, stage: FreshStage): string {
36
+ if (stage === 'none' || text === '') return text
37
+
38
+ return styleText(FORMATS[stage], text)
39
+ }
40
+
41
+ function is_live(frame: FreshFrame): boolean {
42
+ return frame.form === 'screen' && frame.spinner !== undefined
43
+ }
44
+
45
+ // `text` emphasized as a part `age_ms` old, where the frame is a live screen.
46
+ function aged(text: string, age_ms: number | undefined, frame: FreshFrame): string {
47
+ return is_live(frame) ? paint(text, stage_of(age_ms)) : text
48
+ }
49
+
50
+ // `text` emphasized as a part that changed at `at_ms`.
51
+ function since(text: string, at_ms: number | undefined, frame: FreshFrame): string {
52
+ return aged(text, at_ms === undefined ? undefined : frame.now_ms - at_ms, frame)
53
+ }
54
+
55
+ // `text` right-aligned to `width` with only the text emphasized, never the blanks before it, so the
56
+ // column keeps its width and an inverse stage does not light up the padding.
57
+ function padded(text: string, width: number, emphasized: string): string {
58
+ return `${' '.repeat(Math.max(0, width - text.length))}${emphasized}`
59
+ }
60
+
61
+ const run_board_fresh = { aged, padded, paint, since, stage_of }
62
+
63
+ export { run_board_fresh }
64
+ export type { FreshFrame, FreshStage }