@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
@@ -8,7 +8,19 @@ import {
8
8
  } from './agent-role-profile'
9
9
 
10
10
  const AGENT_COMMAND = 'claude'
11
- const AGENT_FLAGS: ReadonlyArray<string> = ['-p', '--verbose', '--output-format', 'stream-json']
11
+ // **The permission mode is passed, not left to the checkout's `.claude/settings.json`.** Claude Code
12
+ // ignores a project's `defaultMode: bypassPermissions` (since v2.1.257) and its `permissions.allow` in
13
+ // a workspace whose trust dialog was never accepted — every fresh clone and every new lane worktree — so
14
+ // a headless child stopped on approvals nobody could give. The flag sets the mode that file declares;
15
+ // its `deny` list stays in effect (#3796).
16
+ const PERMISSION_FLAGS: ReadonlyArray<string> = ['--permission-mode', 'bypassPermissions']
17
+ const AGENT_FLAGS: ReadonlyArray<string> = [
18
+ '-p',
19
+ '--verbose',
20
+ '--output-format',
21
+ 'stream-json',
22
+ ...PERMISSION_FLAGS,
23
+ ]
12
24
  const MODEL_FLAG = '--model'
13
25
  const EFFORT_FLAG = '--effort'
14
26
  // Resumes a stored session by id. An `outage` re-dispatch passes the
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env tsx
2
+ import { fileURLToPath } from 'node:url'
3
+ import { parseArgs } from 'node:util'
4
+ import { execa } from 'execa'
5
+ import { claude_token } from './claude-token'
6
+ import { claude_token_output } from './claude-token-output'
7
+
8
+ // `josh auth:setup` — run `claude setup-token` and keep the token it prints in the macOS keychain,
9
+ // where `josh fresh:run` reads it. The token reaches neither the screen nor a file: it is replaced in
10
+ // the command's output. `--check` answers whether one is saved.
11
+
12
+ const ARGV_OFFSET = 2
13
+ const FAILURE_EXIT_CODE = 1
14
+ const CLAUDE = 'claude'
15
+ const SAVED = `saved: ${claude_token.VARIABLE} (macOS keychain)`
16
+ const NO_TOKEN = 'claude setup-token printed no token, so nothing was saved.'
17
+ const NO_KEYCHAIN = `This machine has no macOS keychain, so nothing was run. Run \`claude setup-token\` yourself and export ${claude_token.VARIABLE} — josh fresh:run reads it when the keychain has none.`
18
+ const NOT_SAVED = 'The keychain did not keep the token. Run `pnpm josh auth:setup` again.'
19
+
20
+ interface Printed {
21
+ decoder: TextDecoder
22
+ text: string
23
+ shown: number
24
+ }
25
+
26
+ function refuse(message: string): number {
27
+ console.error(message)
28
+
29
+ return FAILURE_EXIT_CODE
30
+ }
31
+
32
+ function check(): number {
33
+ const date = claude_token.saved_date()
34
+
35
+ if (date === undefined) {
36
+ return refuse(`missing: no ${claude_token.VARIABLE} saved. ${claude_token.SETUP_HINT}`)
37
+ }
38
+
39
+ console.info(`ok: ${claude_token.VARIABLE} saved on ${date} (macOS keychain)`)
40
+
41
+ return 0
42
+ }
43
+
44
+ function store(token: string | undefined): number {
45
+ if (token === undefined) return refuse(NO_TOKEN)
46
+
47
+ if (!claude_token.save(token)) return refuse(NOT_SAVED)
48
+
49
+ console.info(SAVED)
50
+
51
+ return 0
52
+ }
53
+
54
+ // The sign-in URL and any prompt are shown as they arrive; from the token on, the output is held
55
+ // back and shown once claude ends, with the token replaced.
56
+ function show_live(printed: Printed, chunk: Uint8Array): void {
57
+ printed.text += printed.decoder.decode(chunk, { stream: true })
58
+ const live = claude_token_output.live_length(printed.text)
59
+
60
+ process.stdout.write(printed.text.slice(printed.shown, live))
61
+ printed.shown = live
62
+ }
63
+
64
+ // The browser sign-in needs the terminal's stdin, so only stdout passes through josh.
65
+ async function setup(): Promise<number> {
66
+ if (!claude_token.has_keychain()) return refuse(NO_KEYCHAIN)
67
+
68
+ const printed: Printed = { decoder: new TextDecoder(), text: '', shown: 0 }
69
+ const child = execa(CLAUDE, ['setup-token'], {
70
+ stdin: 'inherit',
71
+ stderr: 'inherit',
72
+ reject: false,
73
+ })
74
+
75
+ child.stdout.on('data', (chunk: Uint8Array) => {
76
+ show_live(printed, chunk)
77
+ })
78
+ const result = await child
79
+ const held = printed.text.slice(printed.shown) + printed.decoder.decode()
80
+ const { token, shown } = claude_token_output.capture(held)
81
+
82
+ process.stdout.write(shown)
83
+
84
+ if (result.failed) return refuse(`claude setup-token failed: ${result.shortMessage ?? CLAUDE}`)
85
+
86
+ return store(token)
87
+ }
88
+
89
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
90
+ const { values } = parseArgs({ args: [...argv], options: { check: { type: 'boolean' } } })
91
+
92
+ return values.check === true ? check() : await setup()
93
+ }
94
+
95
+ const auth_setup_cli = { run }
96
+
97
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
98
+ process.exitCode = await run(process.argv.slice(ARGV_OFFSET))
99
+ }
100
+
101
+ export { auth_setup_cli }
@@ -0,0 +1,78 @@
1
+ import { stripVTControlCharacters } from 'node:util'
2
+
3
+ // What `claude setup-token` printed, split into the token and the text that may be shown: the token
4
+ // is replaced, so it reaches neither the screen nor a log.
5
+
6
+ const REDACTED = '[REDACTED]'
7
+ const TOKEN_START = 'sk-ant-oat'
8
+ const TOKEN_PATTERN = /sk-ant-oat\d+-[\w-]+/u
9
+ const EVERY_TOKEN_PATTERN = new RegExp(TOKEN_PATTERN.source, 'gu')
10
+ // A narrow terminal wraps the token onto lines of its own characters alone, so a continuation is a
11
+ // whole such line and the prose after the token never joins it.
12
+ const CONTINUATION_PATTERN = /^[\w-]+$/u
13
+ const LINE_BREAK_PATTERN = /\r?\n/u
14
+
15
+ interface Captured {
16
+ token: string | undefined
17
+ shown: string
18
+ }
19
+
20
+ // A line that starts a token is another copy of it, never the tail of the one before.
21
+ function is_continuation(line: string): boolean {
22
+ return CONTINUATION_PATTERN.test(line) && !line.startsWith(TOKEN_START)
23
+ }
24
+
25
+ function continuation(lines: ReadonlyArray<string>): Array<string> {
26
+ const length = lines.findIndex((line) => !is_continuation(line.trim()))
27
+
28
+ return (length === -1 ? lines : lines.slice(0, length)).map((line) => line.trim())
29
+ }
30
+
31
+ // Every copy of the token is replaced, its wrapped tail with it: Ink may draw it in more than one frame.
32
+ function redact(lines: ReadonlyArray<string>): Array<string> {
33
+ const start = lines.findIndex((line) => TOKEN_PATTERN.test(line))
34
+
35
+ if (start === -1) return [...lines]
36
+
37
+ const [head = '', ...rest] = lines.slice(start)
38
+ const remaining = rest.slice(continuation(rest).length)
39
+
40
+ return [
41
+ ...lines.slice(0, start),
42
+ head.replaceAll(EVERY_TOKEN_PATTERN, () => REDACTED),
43
+ ...redact(remaining),
44
+ ]
45
+ }
46
+
47
+ function capture(output: string): Captured {
48
+ const lines = stripVTControlCharacters(output).split(LINE_BREAK_PATTERN)
49
+ const start = lines.findIndex((line) => TOKEN_PATTERN.test(line))
50
+ const head = lines[start]
51
+ const shown = redact(lines).join('\n')
52
+
53
+ if (head === undefined) return { token: undefined, shown }
54
+
55
+ const token = [TOKEN_PATTERN.exec(head)?.[0], ...continuation(lines.slice(start + 1))].join('')
56
+
57
+ return { token, shown }
58
+ }
59
+
60
+ // How much of the output so far may be shown as it arrives — the sign-in URL and any prompt come
61
+ // before the token: everything before the token, less a tail that could be the start of one.
62
+ function live_length(output: string): number {
63
+ const start = output.indexOf(TOKEN_START)
64
+
65
+ if (start !== -1) return start
66
+
67
+ const prefixes = Array.from({ length: TOKEN_START.length }, (_, index) =>
68
+ TOKEN_START.slice(0, index + 1),
69
+ )
70
+ const held = prefixes.findLast((prefix) => output.endsWith(prefix))
71
+
72
+ return output.length - (held?.length ?? 0)
73
+ }
74
+
75
+ const claude_token_output = { capture, live_length }
76
+
77
+ export { claude_token_output }
78
+ export type { Captured }
@@ -0,0 +1,68 @@
1
+ import { execaSync } from 'execa'
2
+
3
+ // Where the Claude Code token of `josh fresh:run` lives on the host: the macOS keychain item
4
+ // `josh auth:setup` writes, or — on a machine without a keychain — `CLAUDE_CODE_OAUTH_TOKEN` in the
5
+ // environment. No shell profile is read or written, so no other Claude Code on the host changes how
6
+ // it signs in.
7
+
8
+ const VARIABLE = 'CLAUDE_CODE_OAUTH_TOKEN'
9
+ const SERVICE = 'josh-claude-code-oauth-token'
10
+ const ACCOUNT = 'josh'
11
+ const SECURITY = 'security'
12
+ const SECURITY_TIMEOUT_MS = 10_000
13
+ const FIND = 'find-generic-password'
14
+ const ITEM_ARGUMENTS: ReadonlyArray<string> = ['-s', SERVICE, '-a', ACCOUNT]
15
+ // The modification date of the item, `"mdat"<timedate>=0x… "20261014071518Z\000"`, in UTC.
16
+ const SAVED_DATE_PATTERN = /"mdat"<timedate>=\S+\s+"(\d{4})(\d{2})(\d{2})/u
17
+
18
+ function security(arguments_: ReadonlyArray<string>, input = ''): string | undefined {
19
+ try {
20
+ const { stdout } = execaSync(SECURITY, [...arguments_], { input, timeout: SECURITY_TIMEOUT_MS })
21
+
22
+ return stdout
23
+ } catch {
24
+ return undefined
25
+ }
26
+ }
27
+
28
+ function non_empty(value: string | undefined): string | undefined {
29
+ const trimmed = value?.trim()
30
+
31
+ return trimmed === undefined || trimmed === '' ? undefined : trimmed
32
+ }
33
+
34
+ function from_keychain(): string | undefined {
35
+ return non_empty(security([FIND, ...ITEM_ARGUMENTS, '-w']))
36
+ }
37
+
38
+ // The keychain first, then the environment; `undefined` when neither holds a token.
39
+ function read(): string | undefined {
40
+ return from_keychain() ?? non_empty(process.env[VARIABLE])
41
+ }
42
+
43
+ // `security -i` reads its command from stdin, so the token is in no argument list `ps` could show.
44
+ // The item is read back, because the interactive mode exits 0 whether or not the command succeeded.
45
+ function save(token: string): boolean {
46
+ security(['-i'], `add-generic-password -U ${ITEM_ARGUMENTS.join(' ')} -w "${token}"\n`)
47
+
48
+ return from_keychain() === token
49
+ }
50
+
51
+ // The day the item was last written, `YYYY-MM-DD`; `undefined` when there is none.
52
+ function saved_date(): string | undefined {
53
+ const attributes = security([FIND, ...ITEM_ARGUMENTS])
54
+ const match = attributes === undefined ? undefined : SAVED_DATE_PATTERN.exec(attributes)
55
+
56
+ return match?.slice(1).join('-')
57
+ }
58
+
59
+ // Whether `security` answers at all, asked before a sign-in whose token would have nowhere to go.
60
+ function has_keychain(): boolean {
61
+ return security(['list-keychains']) !== undefined
62
+ }
63
+
64
+ const SETUP_HINT = 'Run `pnpm josh auth:setup` to save one.'
65
+
66
+ const claude_token = { read, save, saved_date, has_keychain, VARIABLE, SETUP_HINT }
67
+
68
+ export { claude_token }
@@ -0,0 +1,125 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { RUN_LANE_LABEL, RUN_SOLO_LABEL } from '#scripts/issue/issue-labels'
3
+ import { issue_number_shape } from '#scripts/issue/issue-number-shape'
4
+ import { session_cite } from '#scripts/issue/session-cite'
5
+ import { cli_flags } from '#scripts/lib/cli-flags'
6
+ import { auto_ok_judge_read } from './auto-ok-judge-read'
7
+ import { auto_ok_judge_write } from './auto-ok-judge-write'
8
+
9
+ // `josh auto-ok:judge` — the open issues `backlogrun` judges for `auto-ok` when a lane is free and
10
+ // nothing is opted in (joshuafolkken/kit#3854), and the record of each judgement.
11
+ //
12
+ // Bare, it lists the issues that still owe a judgement: one number per line on stdout, or `none`, with
13
+ // each title on stderr. The agent reads each one's body and comments (`josh issue:read`) and answers
14
+ // with `--apply <N> --lane|--solo` or `--decline <N> --reason <text>`; which of the two is a reading of
15
+ // the issue, never this command's.
16
+
17
+ const ARGV_OFFSET = 2
18
+ const SUCCESS_EXIT_CODE = 0
19
+ const FAILURE_EXIT_CODE = 1
20
+ const NONE_TOKEN = 'none'
21
+ const USAGE =
22
+ 'Usage: josh auto-ok:judge [--apply <N> (--lane | --solo) | --decline <N> --reason <text>]'
23
+
24
+ const OPTIONS = {
25
+ apply: { type: 'string' },
26
+ decline: { type: 'string' },
27
+ lane: { type: 'boolean' },
28
+ solo: { type: 'boolean' },
29
+ reason: { type: 'string' },
30
+ } as const
31
+
32
+ type Values = NonNullable<ReturnType<typeof cli_flags.values_of<typeof OPTIONS>>>
33
+
34
+ type JudgeRequest =
35
+ | { kind: 'list' }
36
+ | { kind: 'apply'; issue: string; run_label: string }
37
+ | { kind: 'decline'; issue: string; reason: string }
38
+
39
+ function run_label_of(values: Values): string | undefined {
40
+ if (values.lane === values.solo) return undefined
41
+
42
+ return values.lane === true ? RUN_LANE_LABEL : RUN_SOLO_LABEL
43
+ }
44
+
45
+ function apply_request(issue: string, values: Values): JudgeRequest | undefined {
46
+ const run_label = run_label_of(values)
47
+
48
+ return run_label === undefined ? undefined : { kind: 'apply', issue, run_label }
49
+ }
50
+
51
+ function decline_request(issue: string, values: Values): JudgeRequest | undefined {
52
+ const reason = values.reason?.trim() ?? ''
53
+
54
+ return reason === '' ? undefined : { kind: 'decline', issue, reason }
55
+ }
56
+
57
+ function request_of(values: Values): JudgeRequest | undefined {
58
+ const issue = values.apply ?? values.decline
59
+
60
+ if (issue === undefined) return { kind: 'list' }
61
+ if (!issue_number_shape.is_issue_number(issue)) return undefined
62
+
63
+ return values.apply === undefined ? decline_request(issue, values) : apply_request(issue, values)
64
+ }
65
+
66
+ function parse_request(argv: ReadonlyArray<string>): JudgeRequest | undefined {
67
+ const values = cli_flags.values_of(argv, OPTIONS)
68
+
69
+ if (values === undefined || (values.apply !== undefined && values.decline !== undefined)) {
70
+ return undefined
71
+ }
72
+
73
+ return request_of(values)
74
+ }
75
+
76
+ async function list(): Promise<number> {
77
+ const owed = await auto_ok_judge_read.owed()
78
+
79
+ if (owed === undefined) {
80
+ console.error('the open issues could not be read')
81
+
82
+ return FAILURE_EXIT_CODE
83
+ }
84
+
85
+ if (owed.length === 0) console.info(NONE_TOKEN)
86
+
87
+ for (const row of owed) {
88
+ console.error(session_cite.issue(row.number, row.title))
89
+ console.info(String(row.number))
90
+ }
91
+
92
+ return SUCCESS_EXIT_CODE
93
+ }
94
+
95
+ async function dispatch(request: JudgeRequest): Promise<number> {
96
+ const { apply, decline } = auto_ok_judge_write
97
+
98
+ if (request.kind === 'list') return await list()
99
+ if (request.kind === 'decline') return await decline(request.issue, request.reason)
100
+
101
+ return await apply(request.issue, request.run_label)
102
+ }
103
+
104
+ async function run(argv: ReadonlyArray<string>): Promise<number> {
105
+ const request = parse_request(argv)
106
+
107
+ if (request === undefined) {
108
+ console.error(USAGE)
109
+
110
+ return FAILURE_EXIT_CODE
111
+ }
112
+
113
+ return await dispatch(request)
114
+ }
115
+
116
+ // `process.exitCode` rather than `process.exit()`, so the report on standard output drains first.
117
+ async function main(argv: ReadonlyArray<string>): Promise<void> {
118
+ process.exitCode = await run(argv)
119
+ }
120
+
121
+ const auto_ok_judge_cli = { NONE_TOKEN, run }
122
+
123
+ if (process.argv[1] === fileURLToPath(import.meta.url)) await main(process.argv.slice(ARGV_OFFSET))
124
+
125
+ export { auto_ok_judge_cli }
@@ -0,0 +1,95 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { issue_label_schema } from '#scripts/git/git-schemas'
3
+ import { read_json_listing } from '#scripts/git/parse-json-array'
4
+ import { issue_watch_diff } from '#scripts/issue/issue-watch-diff'
5
+ import { bounded_pool } from '#scripts/lib/bounded-pool'
6
+ import { z } from 'zod'
7
+ import { auto_ok_judge, type JudgeIssue } from './auto-ok-judge'
8
+
9
+ // The reading half of `josh auto-ok:judge`: the open issues that still owe a judgement, shared by the
10
+ // command's listing and the backlog driver's check for a free lane with nothing opted in.
11
+ //
12
+ // **The driver asks once a minute while it watches**, so it hands in a cache it keeps for its process:
13
+ // an issue seen judged is remembered with its `updatedAt`, and its comments are read again only once
14
+ // that moves — a body edit, a comment or a label change. Without it every watch minute would read the
15
+ // comments of every candidate in the repository.
16
+
17
+ // The listing cap `auto-ok:next` reads the opted-in pool under; the open backlog is read under the same.
18
+ const LISTING_LIMIT = 200
19
+ // How many comment listings run at once — a handful of `gh` processes, never one per candidate.
20
+ const COMMENT_READ_WIDTH = 4
21
+
22
+ const row_schema = z.looseObject({
23
+ number: z.number(),
24
+ title: z.string(),
25
+ body: z.string().nullish(),
26
+ labels: z.array(issue_label_schema),
27
+ updatedAt: z.string(),
28
+ })
29
+
30
+ interface JudgeRow extends JudgeIssue {
31
+ title: string
32
+ updated_at: string
33
+ }
34
+
35
+ // An issue's number to the `updatedAt` it was last seen judged at.
36
+ type JudgedCache = Map<number, string>
37
+
38
+ function to_row(raw: z.infer<typeof row_schema>): JudgeRow {
39
+ return {
40
+ number: raw.number,
41
+ title: raw.title,
42
+ body: raw.body ?? '',
43
+ labels: raw.labels.map((label) => label.name),
44
+ updated_at: raw.updatedAt,
45
+ }
46
+ }
47
+
48
+ // `undefined` when the listing could not be read, which is never "nothing to judge".
49
+ async function read_candidates(): Promise<ReadonlyArray<JudgeRow> | undefined> {
50
+ const { json } = await git_gh_command.issue_list_open_for_judgement(LISTING_LIMIT)
51
+
52
+ if (json === undefined) return undefined
53
+
54
+ const listing = read_json_listing(json, row_schema)
55
+
56
+ if (listing.kind !== 'read') return undefined
57
+
58
+ return listing.rows.map((raw) => to_row(raw)).filter((row) => auto_ok_judge.is_candidate(row))
59
+ }
60
+
61
+ // `true` only for an issue known to owe a judgement: a failed comment read learns nothing, so it is
62
+ // neither counted nor remembered.
63
+ async function owes_judgement(row: JudgeRow, cache: JudgedCache): Promise<boolean> {
64
+ if (cache.get(row.number) === row.updated_at) return false
65
+
66
+ const comments = issue_watch_diff.parse_comments(
67
+ await git_gh_command.issue_list_comments(String(row.number)),
68
+ )
69
+
70
+ if (comments === undefined) return false
71
+ if (!auto_ok_judge.is_judged(row.body, comments)) return true
72
+
73
+ cache.set(row.number, row.updated_at)
74
+
75
+ return false
76
+ }
77
+
78
+ async function owed(cache: JudgedCache = new Map()): Promise<ReadonlyArray<JudgeRow> | undefined> {
79
+ const candidates = await read_candidates()
80
+
81
+ if (candidates === undefined) return undefined
82
+
83
+ const owing = await bounded_pool.bounded_map(
84
+ candidates,
85
+ COMMENT_READ_WIDTH,
86
+ async (row) => await owes_judgement(row, cache),
87
+ )
88
+
89
+ return candidates.filter((_row, index) => owing[index] === true)
90
+ }
91
+
92
+ const auto_ok_judge_read = { owed }
93
+
94
+ export type { JudgedCache, JudgeRow }
95
+ export { auto_ok_judge_read }
@@ -0,0 +1,109 @@
1
+ import { git_gh_command } from '#scripts/gh/git-gh-command'
2
+ import { issue_label_schema } from '#scripts/git/git-schemas'
3
+ import { parse_json_object_or_undefined } from '#scripts/git/parse-json-array'
4
+ import { issue_auto_ok, type AutoOkDecision } from '#scripts/issue/issue-auto-ok'
5
+ import { issue_cite } from '#scripts/issue/issue-cite'
6
+ import { AUTO_OK_LABEL } from '#scripts/issue/issue-labels'
7
+ import { session_cite } from '#scripts/issue/session-cite'
8
+ import { run_event_stream } from '#scripts/run/event/run-event-stream'
9
+ import { run_event_stream_emit } from '#scripts/run/event/run-event-stream-emit'
10
+ import { z } from 'zod'
11
+ import { auto_ok_judge } from './auto-ok-judge'
12
+
13
+ // The writing half of `josh auto-ok:judge`: the agent's judgement of one open issue, recorded on it.
14
+ //
15
+ // **An applied judgement still passes the mechanical exclusions** — `issue_auto_ok.exclusion_of` is
16
+ // re-read against the labels as they stand, so a label added since the listing refuses the write
17
+ // rather than racing it. **The run label lands before `auto-ok`**, so the pool never holds an opted-in
18
+ // issue that `backlog:next` would withhold as untriaged. Both answers leave the judgement comment the
19
+ // next listing reads as the record, and an applied one is a `note` on the board's 📌.
20
+
21
+ const SUCCESS_EXIT_CODE = 0
22
+ const FAILURE_EXIT_CODE = 1
23
+ const UNREADABLE = 'the issue could not be read'
24
+
25
+ const issue_schema = z.object({ labels: z.array(issue_label_schema), body: z.string().nullish() })
26
+
27
+ interface IssueRead {
28
+ labels: ReadonlyArray<string>
29
+ body: string
30
+ }
31
+
32
+ async function read_issue(issue: string): Promise<IssueRead | undefined> {
33
+ const raw = await git_gh_command.issue_get_labels_and_body(issue)
34
+ const parsed = raw === undefined ? undefined : parse_json_object_or_undefined(raw, issue_schema)
35
+
36
+ if (parsed === undefined) return undefined
37
+
38
+ return { labels: parsed.labels.map((label) => label.name), body: parsed.body ?? '' }
39
+ }
40
+
41
+ async function apply_labels(issue: string, run_label: string): Promise<string | undefined> {
42
+ for (const label of [run_label, AUTO_OK_LABEL]) {
43
+ // eslint-disable-next-line no-await-in-loop -- the run label must land before `auto-ok`
44
+ const write = await git_gh_command.issue_apply_label(issue, label)
45
+
46
+ if (!write.is_applied) return `could not apply \`${label}\`: ${write.reason}`
47
+ }
48
+
49
+ return undefined
50
+ }
51
+
52
+ async function record(issue: string, decision: AutoOkDecision, body: string): Promise<number> {
53
+ try {
54
+ await git_gh_command.issue_comment(issue, auto_ok_judge.comment_of(decision, body))
55
+ } catch {
56
+ console.error(`could not comment the judgement on ${session_cite.issue(issue)}`)
57
+
58
+ return FAILURE_EXIT_CODE
59
+ }
60
+
61
+ console.info(issue_auto_ok.line_of(decision))
62
+
63
+ return SUCCESS_EXIT_CODE
64
+ }
65
+
66
+ function refused(issue: string, problem: string): number {
67
+ console.error(`${session_cite.issue(issue)}: ${problem}`)
68
+
69
+ return FAILURE_EXIT_CODE
70
+ }
71
+
72
+ async function apply_to(issue: string, run_label: string, read: IssueRead): Promise<number> {
73
+ const exclusion = issue_auto_ok.exclusion_of(read.labels)
74
+
75
+ if (exclusion !== undefined) return refused(issue, issue_auto_ok.line_of(exclusion))
76
+
77
+ const problem = await apply_labels(issue, run_label)
78
+
79
+ if (problem !== undefined) return refused(issue, problem)
80
+
81
+ const reason = `judged by backlogrun: no exclusion holds and no person's judgement is needed (${run_label})`
82
+
83
+ await run_event_stream_emit.emit(
84
+ run_event_stream.EVENT_KIND.NOTE,
85
+ `${issue_cite.plain(issue)} auto-ok applied (${run_label})`,
86
+ )
87
+
88
+ return await record(issue, { is_applied: true, reason }, read.body)
89
+ }
90
+
91
+ async function apply(issue: string, run_label: string): Promise<number> {
92
+ const read = await read_issue(issue)
93
+
94
+ if (read === undefined) return refused(issue, UNREADABLE)
95
+
96
+ return await apply_to(issue, run_label, read)
97
+ }
98
+
99
+ async function decline(issue: string, reason: string): Promise<number> {
100
+ const read = await read_issue(issue)
101
+
102
+ if (read === undefined) return refused(issue, UNREADABLE)
103
+
104
+ return await record(issue, { is_applied: false, reason }, read.body)
105
+ }
106
+
107
+ const auto_ok_judge_write = { apply, decline }
108
+
109
+ export { auto_ok_judge_write }
@@ -0,0 +1,67 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { issue_auto_ok, type AutoOkDecision } from '#scripts/issue/issue-auto-ok'
3
+ import { AUTO_OK_LABEL, has_label_name } from '#scripts/issue/issue-labels'
4
+ import type { WatchComment } from '#scripts/issue/issue-watch-diff'
5
+
6
+ // The pure half of `josh auto-ok:judge`: which open issues `backlogrun` judges for `auto-ok` when a lane
7
+ // is free and nothing is opted in, and whether one was already judged (joshuafolkken/kit#3854).
8
+ //
9
+ // **The mechanical exclusions are `issue_auto_ok.exclusion_of`**, the list a filing is judged by
10
+ // (joshuafolkken/kit#3853), never a second one. What a label cannot answer — whether a policy decision,
11
+ // an external account or a human visual check is needed — is the agent's reading of the body and the
12
+ // comments, so this module only says which issues still owe that reading.
13
+ //
14
+ // **The record is the judgement comment itself.** It carries a digest of the body it judged, so an
15
+ // issue is judged again only when its body changed or a person commented after it — the same line
16
+ // `issue_watch_diff` draws between a person's update and an automated one.
17
+
18
+ interface JudgeIssue {
19
+ number: number
20
+ labels: ReadonlyArray<string>
21
+ body: string
22
+ }
23
+
24
+ const MARKER_PREFIX = '<!-- josh:auto-ok-judged body='
25
+ const MARKER_SUFFIX = ' -->'
26
+ const MARKER_PATTERN = /<!-- josh:auto-ok-judged body=(?<digest>[\da-f]+) -->/u
27
+ const DIGEST_LENGTH = 16
28
+ const CRLF = '\r\n'
29
+ const LF = '\n'
30
+
31
+ function is_candidate(issue: JudgeIssue): boolean {
32
+ if (has_label_name(issue.labels, AUTO_OK_LABEL)) return false
33
+
34
+ return issue_auto_ok.exclusion_of(issue.labels) === undefined
35
+ }
36
+
37
+ // GitHub stores a body saved in its web editor with CRLF, so the digest is taken over LF alone.
38
+ function body_digest(body: string): string {
39
+ const normalized = body.split(CRLF).join(LF)
40
+
41
+ return createHash('sha256').update(normalized).digest('hex').slice(0, DIGEST_LENGTH)
42
+ }
43
+
44
+ function digest_of(comment: WatchComment): string | undefined {
45
+ return MARKER_PATTERN.exec(comment.body)?.groups?.['digest']
46
+ }
47
+
48
+ // Judged when the newest judgement comment covers the body as it stands and no person has commented
49
+ // since. A failed comment read never reaches here — the caller treats it as nothing learned.
50
+ function is_judged(body: string, comments: ReadonlyArray<WatchComment>): boolean {
51
+ const judgement = comments.findLast((comment) => digest_of(comment) !== undefined)
52
+
53
+ if (judgement === undefined || digest_of(judgement) !== body_digest(body)) return false
54
+
55
+ return comments.every((comment) => comment.id <= judgement.id || comment.is_automated)
56
+ }
57
+
58
+ function comment_of(decision: AutoOkDecision, body: string): string {
59
+ const marker = `${MARKER_PREFIX}${body_digest(body)}${MARKER_SUFFIX}`
60
+
61
+ return `${issue_auto_ok.line_of(decision)}${LF}${LF}${marker}`
62
+ }
63
+
64
+ const auto_ok_judge = { comment_of, is_candidate, is_judged }
65
+
66
+ export type { JudgeIssue }
67
+ export { auto_ok_judge }