tumwater 0.4.1 → 0.4.3

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 (338) hide show
  1. package/README.md +3 -5
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +35 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +70 -37
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +24 -24
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +6 -14
  8. package/dist/src/agent/backend-traits.js +16 -2
  9. package/dist/src/agent/claude/claude-account.js +1 -8
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +131 -78
  13. package/dist/src/agent/codex/codex-account.js +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +120 -69
  18. package/dist/src/agent/codex/codex-stream.js +72 -39
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +21 -33
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +52 -29
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +51 -29
  27. package/dist/src/agent/session-ledger.js +16 -0
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-archive.js +64 -165
  30. package/dist/src/backlog/backlog-cli-view.js +81 -41
  31. package/dist/src/backlog/backlog-cli.js +57 -26
  32. package/dist/src/backlog/backlog-eligibility.js +84 -136
  33. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  34. package/dist/src/backlog/backlog-index-data.js +2 -2
  35. package/dist/src/backlog/backlog-md.js +42 -31
  36. package/dist/src/backlog/backlog-parts.js +65 -27
  37. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +30 -14
  38. package/dist/src/backlog/backlog-release.js +15 -23
  39. package/dist/src/backlog/backlog-structure.js +128 -62
  40. package/dist/src/backlog/backlog-write.js +1 -1
  41. package/dist/src/backlog/backlog.js +5 -4
  42. package/dist/src/baseline/main-baseline.js +178 -104
  43. package/dist/src/baseline/main-guardrails.js +54 -17
  44. package/dist/src/baseline/main-watch.js +1 -1
  45. package/dist/src/budget/budget-fallback-view.js +100 -243
  46. package/dist/src/budget/budget-wiring.js +24 -65
  47. package/dist/src/budget/budget.js +2 -1
  48. package/dist/src/budget/fallback-breaker.js +2 -2
  49. package/dist/src/build/build-check-detect.js +43 -14
  50. package/dist/src/build/build-check-events.js +2 -2
  51. package/dist/src/build/build-check-scoped.js +103 -50
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/change/change-data.js +4 -2
  54. package/dist/src/cli/cli-command-args.js +275 -137
  55. package/dist/src/cli/cli-flag-specs.js +43 -26
  56. package/dist/src/cli/cli-init.js +34 -12
  57. package/dist/src/cli/cli-query-commands.js +1 -1
  58. package/dist/src/cli/cli-repo-gate.js +2 -2
  59. package/dist/src/cli/cli-review-commands.js +2 -1
  60. package/dist/src/cli/cli-role-command.js +27 -50
  61. package/dist/src/cli/cli-start.js +150 -67
  62. package/dist/src/cli/completion.js +82 -46
  63. package/dist/src/cli/config-commands.js +82 -71
  64. package/dist/src/cli/follow-view.js +3 -1
  65. package/dist/src/cli/help.js +6 -9
  66. package/dist/src/cli/log-commands.js +46 -24
  67. package/dist/src/cli/notify-command.js +2 -2
  68. package/dist/src/cli/question-commands.js +86 -60
  69. package/dist/src/cli.js +183 -141
  70. package/dist/src/collections.js +52 -23
  71. package/dist/src/concurrency/check-slots.js +70 -0
  72. package/dist/src/concurrency/lock.js +22 -0
  73. package/dist/src/concurrency/run-permit.js +49 -0
  74. package/dist/src/concurrency/semaphore.js +46 -250
  75. package/dist/src/config/config-editable-keys.js +4 -7
  76. package/dist/src/config/config-fallbacks.js +79 -103
  77. package/dist/src/config/config-field-checks.js +66 -37
  78. package/dist/src/config/config-keys.js +5 -20
  79. package/dist/src/config/config-live.js +161 -153
  80. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  81. package/dist/src/config/config-request.js +153 -102
  82. package/dist/src/config/config-schema.js +9 -11
  83. package/dist/src/config/config-selector-checks.js +1 -1
  84. package/dist/src/config/config-validation-agents.js +2 -2
  85. package/dist/src/config/config-validation-roles.js +4 -52
  86. package/dist/src/config/config-validation.js +4 -38
  87. package/dist/src/config/config-views.js +4 -34
  88. package/dist/src/config/config-write.js +22 -46
  89. package/dist/src/config/config.js +4 -5
  90. package/dist/src/doctor/doctor-backlog-bugs.js +24 -121
  91. package/dist/src/doctor/doctor-backlog-plans.js +27 -27
  92. package/dist/src/doctor/doctor-backlog.js +29 -41
  93. package/dist/src/doctor/doctor-check-names.js +4 -7
  94. package/dist/src/doctor/doctor-checks.js +5 -5
  95. package/dist/src/doctor/doctor-claude-checks.js +46 -30
  96. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  97. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  98. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  99. package/dist/src/doctor/doctor-holds.js +19 -56
  100. package/dist/src/doctor/doctor-install.js +1 -1
  101. package/dist/src/doctor/doctor-launch-services.js +1 -1
  102. package/dist/src/doctor/doctor-model-checks.js +181 -273
  103. package/dist/src/doctor/doctor-notify.js +4 -7
  104. package/dist/src/doctor/doctor-opencode-checks.js +90 -52
  105. package/dist/src/doctor/doctor-orphans.js +120 -20
  106. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  107. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  108. package/dist/src/doctor/doctor-principles.js +3 -3
  109. package/dist/src/doctor/{probe.js → doctor-probe.js} +1 -1
  110. package/dist/src/doctor/doctor-project-checks.js +69 -20
  111. package/dist/src/doctor/doctor-role-notes.js +7 -25
  112. package/dist/src/doctor/doctor.js +12 -15
  113. package/dist/src/events/event-format.js +19 -53
  114. package/dist/src/events/event-labels.js +0 -5
  115. package/dist/src/events/event-legacy.js +0 -2
  116. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  117. package/dist/src/events/event-read.js +1 -1
  118. package/dist/src/events/event-window.js +1 -1
  119. package/dist/src/events/events.js +15 -2
  120. package/dist/src/failure/failure-cluster.js +6 -6
  121. package/dist/src/failure/failure-data.js +8 -4
  122. package/dist/src/failure/failure-render.js +44 -34
  123. package/dist/src/failure/failure-state-change.js +177 -92
  124. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  125. package/dist/src/files/files.js +30 -0
  126. package/dist/src/files/json-object.js +21 -8
  127. package/dist/src/files/stat-cache.js +12 -8
  128. package/dist/src/gates/budget-gates.js +85 -84
  129. package/dist/src/gates/gate-polls.js +27 -68
  130. package/dist/src/gates/pause-gates.js +16 -2
  131. package/dist/src/gates/readiness.js +1 -1
  132. package/dist/src/gates/role-cap-gates.js +44 -35
  133. package/dist/src/gates/{startup-gate.js → startup-gates.js} +34 -34
  134. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  135. package/dist/src/git/git.js +75 -45
  136. package/dist/src/git/slot-salvage.js +5 -4
  137. package/dist/src/git/worktree-pool.js +3 -6
  138. package/dist/src/gui/gui-admin-endpoints.js +5 -6
  139. package/dist/src/gui/gui-backlog-commands.js +4 -4
  140. package/dist/src/gui/gui-backlog-endpoints.js +62 -48
  141. package/dist/src/gui/gui-command.js +43 -24
  142. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  143. package/dist/src/gui/gui-history-endpoints.js +100 -52
  144. package/dist/src/gui/gui-operator-commands.js +22 -35
  145. package/dist/src/gui/gui-prompt-commands.js +48 -42
  146. package/dist/src/gui/gui-report-endpoints.js +3 -2
  147. package/dist/src/gui/gui-server.js +1 -2
  148. package/dist/src/history/history-data.js +71 -44
  149. package/dist/src/history/history-queued.js +49 -27
  150. package/dist/src/history/history.js +138 -85
  151. package/dist/src/inbox/inbox-submit.js +1 -5
  152. package/dist/src/inbox/prompt-commands.js +82 -60
  153. package/dist/src/init/init.js +156 -55
  154. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  155. package/dist/src/merge/merge-core.js +2 -2
  156. package/dist/src/merge/merge-drain.js +8 -11
  157. package/dist/src/merge/merge-pipeline.js +6 -7
  158. package/dist/src/merge/merge-post-review-drain.js +11 -14
  159. package/dist/src/merge/merge-requeue.js +1 -1
  160. package/dist/src/merge/merge-slot.js +0 -8
  161. package/dist/src/merge/merge-verify.js +8 -18
  162. package/dist/src/notes-cli.js +52 -42
  163. package/dist/src/operator/operator-commands.js +143 -90
  164. package/dist/src/operator/operator-intent.js +3 -3
  165. package/dist/src/operator/{retire.js → operator-retire.js} +2 -1
  166. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  167. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  168. package/dist/src/orchestrator/orchestrator.js +8 -21
  169. package/dist/src/orchestrator/retention.js +6 -5
  170. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  171. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  172. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  173. package/dist/src/orchestrator/startup-migrations.js +1 -1
  174. package/dist/src/paths.js +8 -13
  175. package/dist/src/pi/pi-args.js +5 -2
  176. package/dist/src/pi/pi-backend.js +2 -2
  177. package/dist/src/pi/pi-bin.js +3 -3
  178. package/dist/src/pi/pi-event-line.js +1 -1
  179. package/dist/src/pi/pi-models.js +43 -29
  180. package/dist/src/pi/pi-settings.js +43 -20
  181. package/dist/src/pi/pi-stream.js +32 -6
  182. package/dist/src/pi/pi-watchdogs.js +29 -3
  183. package/dist/src/pi/pi.js +2 -2
  184. package/dist/src/pi-extension/context-shake.js +87 -51
  185. package/dist/src/pi-extension/kill-guard.js +69 -0
  186. package/dist/src/process/process-group.js +49 -5
  187. package/dist/src/process/process.js +31 -8
  188. package/dist/src/process/run-marker.js +31 -13
  189. package/dist/src/process/supervisor.js +57 -29
  190. package/dist/src/prompt/dialect.js +1 -1
  191. package/dist/src/prompt/gate-notes.js +11 -11
  192. package/dist/src/prompt/notes-backlog.js +73 -57
  193. package/dist/src/prompt/principles.js +6 -4
  194. package/dist/src/prompt/prompt.js +92 -43
  195. package/dist/src/report/report-data.js +61 -260
  196. package/dist/src/report/report-fold.js +34 -302
  197. package/dist/src/report/report-main-red.js +45 -42
  198. package/dist/src/report/report-render.js +37 -183
  199. package/dist/src/report/report-since-data.js +219 -0
  200. package/dist/src/report/report-since-render.js +173 -0
  201. package/dist/src/report/report-usage-fold.js +288 -0
  202. package/dist/src/report/report.js +48 -23
  203. package/dist/src/review/known-flakes.js +3 -3
  204. package/dist/src/review/post-merge-review-accept.js +3 -2
  205. package/dist/src/review/post-merge-review-render.js +9 -3
  206. package/dist/src/review/post-merge-review.js +10 -10
  207. package/dist/src/review/quarantine-render.js +2 -1
  208. package/dist/src/review/review-precheck.js +11 -1
  209. package/dist/src/review/review.js +40 -153
  210. package/dist/src/review/secret-leaks.js +26 -16
  211. package/dist/src/roles/role-catalog.js +28 -32
  212. package/dist/src/roles/role-guidance.js +25 -46
  213. package/dist/src/roles/role-render.js +1 -14
  214. package/dist/src/roles/role-view.js +10 -23
  215. package/dist/src/run/acceptance-greps.js +88 -45
  216. package/dist/src/run/ci-parity.js +5 -6
  217. package/dist/src/run/qa-coverage.js +4 -4
  218. package/dist/src/run/run-apply.js +12 -6
  219. package/dist/src/run/run-detail-data.js +137 -68
  220. package/dist/src/run/run-detail-render.js +97 -34
  221. package/dist/src/run/run-detail.js +106 -53
  222. package/dist/src/run/run-dismiss.js +43 -0
  223. package/dist/src/run/run-prompt.js +150 -93
  224. package/dist/src/run/run-resume.js +85 -37
  225. package/dist/src/run/run-stage.js +139 -105
  226. package/dist/src/run/run-timing.js +26 -6
  227. package/dist/src/run/run-usage.js +31 -1
  228. package/dist/src/run/run-verdict.js +71 -67
  229. package/dist/src/run/stage-backlog-checks.js +2 -29
  230. package/dist/src/run/stage-check.js +52 -43
  231. package/dist/src/run/stage-checks-shared.js +28 -0
  232. package/dist/src/run/stage-name-checks.js +111 -92
  233. package/dist/src/run/stage-path-checks.js +69 -42
  234. package/dist/src/run/stage-prose-checks.js +55 -40
  235. package/dist/src/runner/runner-outcome.js +2 -2
  236. package/dist/src/runner/runner-pi.js +3 -10
  237. package/dist/src/runner/runner-recovery.js +1 -1
  238. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  239. package/dist/src/runner/runner-run.js +23 -38
  240. package/dist/src/runner/runner-state-load.js +2 -9
  241. package/dist/src/runner/runner.js +21 -36
  242. package/dist/src/scheduling/claims.js +3 -14
  243. package/dist/src/scheduling/scheduling.js +53 -66
  244. package/dist/src/status/status-data.js +7 -11
  245. package/dist/src/status/status-role-notes.js +20 -16
  246. package/dist/src/status/status-roles.js +11 -52
  247. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  248. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  249. package/dist/src/team/team-hold.js +98 -65
  250. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  251. package/dist/src/team/team-polls.js +28 -126
  252. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  253. package/dist/src/team/team-state.js +4 -13
  254. package/dist/src/text/datetime.js +8 -1
  255. package/dist/src/text/markdown.js +1 -1
  256. package/dist/src/text/phrases.js +20 -15
  257. package/dist/src/text/text.js +15 -3
  258. package/dist/src/ui/alert-feeds.js +52 -281
  259. package/dist/src/ui/badges.js +3 -14
  260. package/dist/src/ui/gui/gui-client-boot.js +21 -6
  261. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  262. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  263. package/dist/src/ui/gui/gui-client-drawer.js +2 -2
  264. package/dist/src/ui/gui/gui-client-history.js +28 -14
  265. package/dist/src/ui/gui/gui-client-model.js +0 -12
  266. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  267. package/dist/src/ui/gui/gui-client-report-chart.js +12 -10
  268. package/dist/src/ui/gui/gui-client-report.js +1 -2
  269. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  270. package/dist/src/ui/gui/gui-client-settings.js +43 -107
  271. package/dist/src/ui/gui/gui-client-team.js +30 -4
  272. package/dist/src/ui/gui/gui-client.js +7 -1
  273. package/dist/src/ui/gui/gui-icons.js +7 -1
  274. package/dist/src/ui/progress-data.js +53 -40
  275. package/dist/src/ui/run-progress-model.js +9 -2
  276. package/dist/src/ui/status-model.js +4 -26
  277. package/dist/src/ui/status-payload.js +4 -8
  278. package/dist/src/ui/status-render.js +6 -10
  279. package/dist/src/ui/team-alert-backlog.js +29 -60
  280. package/dist/src/ui/team-alert-shared.js +23 -10
  281. package/dist/src/ui/team-alert-status.js +17 -173
  282. package/dist/src/ui/team-alert-tail.js +19 -228
  283. package/dist/src/ui/team-alerts.js +12 -24
  284. package/dist/src/ui/transcript-run.js +27 -16
  285. package/dist/src/ui/transcript-tail.js +137 -94
  286. package/dist/src/ui/tui/tui-editor-answer.js +66 -54
  287. package/dist/src/ui/tui/tui-editor-attach.js +77 -65
  288. package/dist/src/ui/tui/tui-editor-backends.js +65 -58
  289. package/dist/src/ui/tui/tui-editor-brief.js +64 -57
  290. package/dist/src/ui/tui/tui-editor-budget.js +109 -96
  291. package/dist/src/ui/tui/tui-editor-config.js +81 -71
  292. package/dist/src/ui/tui/tui-editor-entry.js +167 -157
  293. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  294. package/dist/src/ui/tui/tui-editor-pause.js +208 -188
  295. package/dist/src/ui/tui/tui-editor-prompt.js +50 -44
  296. package/dist/src/ui/tui/tui-editor-queued.js +74 -67
  297. package/dist/src/ui/tui/tui-editor-quiet.js +124 -110
  298. package/dist/src/ui/tui/tui-editor-release.js +53 -44
  299. package/dist/src/ui/tui/tui-editor-retire.js +60 -49
  300. package/dist/src/ui/tui/tui-editor-role-enabled.js +93 -82
  301. package/dist/src/ui/tui/tui-editor-role-file.js +61 -49
  302. package/dist/src/ui/tui/tui-editor-role-key.js +52 -44
  303. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  304. package/dist/src/ui/tui/tui-editor-search.js +73 -60
  305. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  306. package/dist/src/ui/tui/tui-editors.js +1 -7
  307. package/dist/src/ui/tui/tui-frame.js +182 -134
  308. package/dist/src/ui/tui/tui-guard.js +20 -0
  309. package/dist/src/ui/tui/tui-input.js +159 -92
  310. package/dist/src/ui/tui/tui-keymap.js +51 -29
  311. package/dist/src/ui/tui/tui-keys.js +2 -2
  312. package/dist/src/ui/tui/tui-pane-activity.js +65 -53
  313. package/dist/src/ui/tui/tui-pane-backlog.js +78 -40
  314. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  315. package/dist/src/ui/tui/tui-pane-health.js +254 -251
  316. package/dist/src/ui/tui/tui-pane-history.js +81 -94
  317. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  318. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  319. package/dist/src/ui/tui/tui-pane-report.js +76 -58
  320. package/dist/src/ui/tui/tui-pane-session.js +14 -9
  321. package/dist/src/ui/tui/tui-pane-transcript.js +152 -141
  322. package/dist/src/ui/tui/tui-pane.js +205 -175
  323. package/dist/src/ui/tui/tui-panes.js +31 -35
  324. package/dist/src/ui/tui/tui.js +4 -5
  325. package/dist/src/verdict/fix-claim.js +87 -53
  326. package/dist/src/verdict/refusal.js +31 -7
  327. package/package.json +5 -5
  328. package/dist/src/concurrency/provider-lease.js +0 -105
  329. package/dist/src/doctor/image-probe.js +0 -257
  330. package/dist/src/runner/model-fallback.js +0 -76
  331. package/dist/src/runner/runner-fallback.js +0 -66
  332. package/dist/src/ui/team-alert-backlog-feed.js +0 -181
  333. package/dist/src/ui/team-alert-merge.js +0 -183
  334. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  335. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  336. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  337. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  338. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -11,13 +11,13 @@
11
11
  * Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
12
12
  * state (the pending split-block assistant message, the unmapped native types seen, whether a
13
13
  * `run_cost` arrived), so call it once per run. Unmapped native types are recorded once each
14
- * on the seam's optional `warnings()` — src/agent/run-stream.ts forwards each as an
14
+ * on the seam's optional `warnings()` — src/agent/agent-stream.ts forwards each as an
15
15
  * `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
16
16
  * run produced no authoritative `run_cost`. */
17
17
  import { joinParts } from "../../text/text.js";
18
- import { secondsUntil } from "../../text/datetime.js";
19
- import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
20
- import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
18
+ import { secondsUntilUnixStamp } from "../../text/datetime.js";
19
+ import { canonicalUsageBlock, isJsonObject, nonNegativeNumber, objectOrEmpty, optionalString, parseJsonObject } from "../../files/json-object.js";
20
+ import { SERVER_TRANSIENT_STATUS, transientProseKind } from "../backend-traits.js";
21
21
  /** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
22
22
  * lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
23
23
  * lower-cased so a future tool still reads as a name rather than as `undefined`. */
@@ -32,9 +32,6 @@ const TOOL_NAMES = {
32
32
  };
33
33
  /** claude's HTTP status classes that mean a permanent configuration error. */
34
34
  const CONFIG_STATUS = new Set([400, 401, 403, 404]);
35
- function str(value) {
36
- return typeof value === "string" ? value : undefined;
37
- }
38
35
  /** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
39
36
  * authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
40
37
  * on every event, so summing per event would double-count — the fold takes it once). The
@@ -49,15 +46,15 @@ function canonicalUsage(usage) {
49
46
  function canonicalBlock(block) {
50
47
  switch (block.type) {
51
48
  case "text":
52
- return { type: "text", text: str(block.text) ?? "" };
49
+ return { type: "text", text: optionalString(block.text) ?? "" };
53
50
  case "thinking":
54
- return { type: "thinking", thinking: str(block.thinking) ?? str(block.text) ?? "" };
51
+ return { type: "thinking", thinking: optionalString(block.thinking) ?? optionalString(block.text) ?? "" };
55
52
  case "tool_use": {
56
53
  const input = objectOrEmpty(block.input);
57
54
  const args = { ...input };
58
55
  if (typeof input.file_path === "string")
59
56
  args.path = input.file_path;
60
- const name = str(block.name) ?? "";
57
+ const name = optionalString(block.name) ?? "";
61
58
  // Own-property test, not a bare index: a tool name like `constructor`, `toString`, or
62
59
  // `__proto__` would otherwise inherit a function (or Object.prototype) from the table's
63
60
  // prototype chain and bypass the lower-cased fallback — JSON.stringify then drops the
@@ -65,7 +62,7 @@ function canonicalBlock(block) {
65
62
  // applies to EFFORT_BY_THINKING.
66
63
  return {
67
64
  type: "toolCall",
68
- id: str(block.id) ?? "",
65
+ id: optionalString(block.id) ?? "",
69
66
  name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
70
67
  arguments: args,
71
68
  };
@@ -74,48 +71,80 @@ function canonicalBlock(block) {
74
71
  return undefined;
75
72
  }
76
73
  }
77
- /** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
78
- * a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
79
- * when the result is not an error or carries no signal the vocabulary names — a SIGTERM
80
- * (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
81
- * provider classification applies. `status` is `api_error_status`, which is null on the
82
- * not-logged-in result, so the text checks carry that case. */
83
- function classifyResult(result) {
84
- const terminal = str(result.terminal_reason);
74
+ /** `terminal === "aborted_tools"` is deliberate: the harness killed the run, so no provider
75
+ * classification applies — the chain stops there. `error_max_budget_usd`, as a terminal
76
+ * reason or in the text, caps the budget. */
77
+ function terminalAndBudgetClassification(terminal, text) {
85
78
  if (terminal === "aborted_tools")
86
79
  return {};
87
80
  if (terminal === "error_max_budget_usd")
88
81
  return { kind: "budgetCapped" };
89
- const text = joinParts([str(result.result), str(result.error)], " ");
90
82
  if (/error_max_budget_usd/i.test(text))
91
83
  return { kind: "budgetCapped" };
92
- const status = nonNegativeNumber(result.api_error_status, undefined);
84
+ return undefined;
85
+ }
86
+ function statusClassification(status) {
93
87
  if (status === 429)
94
88
  return { kind: "transientRateLimit" };
95
89
  if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
96
90
  return { kind: "transientBackend", backendKind: "server" };
97
- // The context-overflow text wins over a configuration status: claude reports an oversized
98
- // prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
91
+ return undefined;
92
+ }
93
+ /** The context-overflow text wins over a configuration status: claude reports an oversized
94
+ * prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError. */
95
+ function contextOverflowClassification(text) {
99
96
  if (/prompt is too long/i.test(text))
100
97
  return { kind: "contextExceeded" };
98
+ return undefined;
99
+ }
100
+ function configStatusClassification(status) {
101
101
  if (status !== undefined && CONFIG_STATUS.has(status))
102
102
  return { kind: "configError" };
103
- // Text fallbacks, for the statuses claude leaves null or omits.
103
+ return undefined;
104
+ }
105
+ function resumeMissingClassification(text) {
104
106
  if (/No conversation found with session ID/i.test(text))
105
107
  return { kind: "resumeMissing" };
108
+ return undefined;
109
+ }
110
+ function configTextClassification(text) {
106
111
  if (/Not logged in|issue with the selected model|does not support this model/i.test(text))
107
112
  return { kind: "configError" };
113
+ return undefined;
114
+ }
115
+ function transientTextClassification(text) {
108
116
  if (/\b429\b|rate limit/i.test(text))
109
117
  return { kind: "transientRateLimit" };
110
118
  if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
111
119
  return { kind: "transientBackend", backendKind: "server" };
112
- if (/connection|econn|refused|reset|terminated/i.test(text))
113
- return { kind: "transientBackend", backendKind: "connection" };
114
- if (/timed out|timeout/i.test(text))
115
- return { kind: "transientBackend", backendKind: "timeout" };
120
+ const prose = transientProseKind(text);
121
+ return prose;
122
+ }
123
+ /** Text fallbacks, for the statuses claude leaves null or omits. */
124
+ function httpStatusTextClassification(text) {
116
125
  if (/\b(400|401|403|404)\b/i.test(text))
117
126
  return { kind: "configError" };
118
- return {};
127
+ return undefined;
128
+ }
129
+ /** How a `result` event's error signals classify into the canonical `tumwaterKind` (and, for
130
+ * a transient backend, its kind), trusted by `PiStreamParser` before its text regexes. Empty
131
+ * when the result is not an error or carries no signal the vocabulary names — a SIGTERM
132
+ * (`terminal_reason: "aborted_tools"`) is deliberate: the harness killed the run, so no
133
+ * provider classification applies. `status` is `api_error_status`, which is null on the
134
+ * not-logged-in result, so the text checks carry that case. */
135
+ function classifyResult(result) {
136
+ const terminal = optionalString(result.terminal_reason);
137
+ const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
138
+ const status = nonNegativeNumber(result.api_error_status, undefined);
139
+ return (terminalAndBudgetClassification(terminal, text) ??
140
+ statusClassification(status) ??
141
+ contextOverflowClassification(text) ??
142
+ configStatusClassification(status) ??
143
+ resumeMissingClassification(text) ??
144
+ configTextClassification(text) ??
145
+ transientTextClassification(text) ??
146
+ httpStatusTextClassification(text) ??
147
+ {});
119
148
  }
120
149
  /** Build a fresh per-run claude translator. */
121
150
  export function claudeTranslator(opts = {}) {
@@ -169,14 +198,14 @@ export function claudeTranslator(opts = {}) {
169
198
  warnings.add(kind);
170
199
  };
171
200
  const handleSystem = (event) => {
172
- const subtype = str(event.subtype);
201
+ const subtype = optionalString(event.subtype);
173
202
  if (subtype === "init") {
174
203
  const out = [];
175
- const cwd = str(event.cwd);
176
- const id = str(event.session_id);
204
+ const cwd = optionalString(event.cwd);
205
+ const id = optionalString(event.session_id);
177
206
  out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
178
207
  out.push(JSON.stringify({ type: "agent_start" }));
179
- const billing = billingOf(str(event.apiKeySource), opts.thirdPartyProvider === true);
208
+ const billing = billingOf(optionalString(event.apiKeySource), opts.thirdPartyProvider === true);
180
209
  if (billing)
181
210
  out.push(JSON.stringify({ type: "billing", billing }));
182
211
  return out;
@@ -185,7 +214,7 @@ export function claudeTranslator(opts = {}) {
185
214
  const attempt = nonNegativeNumber(event.attempt, undefined);
186
215
  const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
187
216
  const status = nonNegativeNumber(event.error_status, undefined);
188
- const error = str(event.error) ?? "";
217
+ const error = optionalString(event.error) ?? "";
189
218
  const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
190
219
  return [
191
220
  JSON.stringify({
@@ -207,7 +236,7 @@ export function claudeTranslator(opts = {}) {
207
236
  const handleAssistant = (event) => {
208
237
  sawActivity = true;
209
238
  const message = objectOrEmpty(event.message);
210
- const id = str(message.id) ?? `anon-${anonymous++}`;
239
+ const id = optionalString(message.id) ?? `anon-${anonymous++}`;
211
240
  // A new id flushes the open message; a continuing one flushes nothing. Either way the
212
241
  // fresh or open pending then folds this event's content, usage, and stop reason the
213
242
  // same way — that shared fold is why both branches live on one path.
@@ -230,7 +259,7 @@ export function claudeTranslator(opts = {}) {
230
259
  // Usage repeats on every event of the same message; the last read wins and is counted
231
260
  // once because the fold emits one message_end per id.
232
261
  pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
233
- const stopReason = str(message.stop_reason);
262
+ const stopReason = optionalString(message.stop_reason);
234
263
  if (stopReason !== undefined)
235
264
  pending.stopReason = stopReason;
236
265
  return out;
@@ -244,7 +273,7 @@ export function claudeTranslator(opts = {}) {
244
273
  continue;
245
274
  out.push(JSON.stringify({
246
275
  type: "tool_execution_end",
247
- toolCallId: str(raw.tool_use_id) ?? "",
276
+ toolCallId: optionalString(raw.tool_use_id) ?? "",
248
277
  isError: raw.is_error === true,
249
278
  }));
250
279
  }
@@ -252,45 +281,12 @@ export function claudeTranslator(opts = {}) {
252
281
  };
253
282
  const handleRateLimitEvent = (event) => {
254
283
  const info = objectOrEmpty(event.rate_limit_info);
255
- const rateLimitType = str(info.rateLimitType);
256
- const status = str(info.status);
257
- // claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
258
- // keyed by window type, and only the binding window's status at the top level.
259
- const windows = [];
260
- for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
261
- if (type === rateLimitType || !isJsonObject(raw))
262
- continue;
263
- const reading = { type: "rate_limit", rateLimitType: type };
264
- const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
265
- if (windowResets !== undefined)
266
- reading.resetsAt = windowResets;
267
- const windowUtil = nonNegativeNumber(raw.utilization, undefined);
268
- if (windowUtil !== undefined)
269
- reading.utilization = windowUtil;
270
- windows.push(JSON.stringify(reading));
271
- }
272
- const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
273
- const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
274
- const out = { type: "rate_limit" };
275
- if (rateLimitType !== undefined)
276
- out.rateLimitType = rateLimitType;
277
- if (status !== undefined)
278
- out.status = status;
279
- if (resetsAt !== undefined)
280
- out.resetsAt = resetsAt;
281
- const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
282
- if (utilization !== undefined)
283
- out.utilization = utilization;
284
- const overageStatus = str(info.overageStatus);
285
- if (overageStatus !== undefined)
286
- out.overageStatus = overageStatus;
287
- if (info.isUsingOverage === true)
288
- out.isUsingOverage = true;
289
- if (status === "rejected" && resetsAt !== undefined) {
290
- // resetsAt is a unix-seconds stamp; secondsUntil folds it (×1000) to whole seconds.
291
- out.retryAfterSeconds = secondsUntil(resetsAt * 1000, Date.now());
292
- }
293
- return [JSON.stringify(out), ...windows];
284
+ const rateLimitType = optionalString(info.rateLimitType);
285
+ const status = optionalString(info.status);
286
+ return [
287
+ JSON.stringify(claudeRateLimitReading(info, rateLimitType, status)),
288
+ ...claudeRateLimitWindowReadings(info, rateLimitType),
289
+ ];
294
290
  };
295
291
  const handleResult = (event) => {
296
292
  sawActivity = true;
@@ -315,7 +311,7 @@ export function claudeTranslator(opts = {}) {
315
311
  content: [],
316
312
  stopReason: "error",
317
313
  };
318
- const errorMessage = str(event.result) ?? str(event.error);
314
+ const errorMessage = optionalString(event.result) ?? optionalString(event.error);
319
315
  if (errorMessage !== undefined)
320
316
  message.errorMessage = errorMessage;
321
317
  // tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
@@ -334,7 +330,7 @@ export function claudeTranslator(opts = {}) {
334
330
  if (!parsed)
335
331
  return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
336
332
  const event = parsed;
337
- const type = str(event.type);
333
+ const type = optionalString(event.type);
338
334
  switch (type) {
339
335
  case "system": return handleSystem(event);
340
336
  case "assistant": return handleAssistant(event);
@@ -367,6 +363,63 @@ export function claudeTranslator(opts = {}) {
367
363
  },
368
364
  };
369
365
  }
366
+ /** One reading per sibling `unifiedWindows` window other than the binding window. */
367
+ function claudeRateLimitWindowReadings(info, rateLimitType) {
368
+ // claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
369
+ // keyed by window type, and only the binding window's status at the top level.
370
+ const windows = [];
371
+ for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
372
+ if (type === rateLimitType || !isJsonObject(raw))
373
+ continue;
374
+ const reading = { type: "rate_limit", rateLimitType: type };
375
+ const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
376
+ if (windowResets !== undefined)
377
+ reading.resetsAt = windowResets;
378
+ const windowUtil = nonNegativeNumber(raw.utilization, undefined);
379
+ if (windowUtil !== undefined)
380
+ reading.utilization = windowUtil;
381
+ windows.push(JSON.stringify(reading));
382
+ }
383
+ return windows;
384
+ }
385
+ /** The binding window's own `unifiedWindows` record, if a rate-limit type was named. */
386
+ function claudeRateLimitOwnWindow(info, rateLimitType) {
387
+ return rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
388
+ }
389
+ /** The binding window's reset time: the top-level `resetsAt` falling back to its own record. */
390
+ function claudeRateLimitResetsAt(info, own) {
391
+ return nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
392
+ }
393
+ /** The binding window's utilization: the top-level `utilization` falling back to its own record. */
394
+ function claudeRateLimitUtilization(info, own) {
395
+ return nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
396
+ }
397
+ /** The binding window's canonical reading: its optional fields, plus a rejected window's
398
+ * retry-after. */
399
+ function claudeRateLimitReading(info, rateLimitType, status) {
400
+ const own = claudeRateLimitOwnWindow(info, rateLimitType);
401
+ const resetsAt = claudeRateLimitResetsAt(info, own);
402
+ const out = { type: "rate_limit" };
403
+ if (rateLimitType !== undefined)
404
+ out.rateLimitType = rateLimitType;
405
+ if (status !== undefined)
406
+ out.status = status;
407
+ if (resetsAt !== undefined)
408
+ out.resetsAt = resetsAt;
409
+ const utilization = claudeRateLimitUtilization(info, own);
410
+ if (utilization !== undefined)
411
+ out.utilization = utilization;
412
+ const overageStatus = optionalString(info.overageStatus);
413
+ if (overageStatus !== undefined)
414
+ out.overageStatus = overageStatus;
415
+ if (info.isUsingOverage === true)
416
+ out.isUsingOverage = true;
417
+ if (status === "rejected" && resetsAt !== undefined) {
418
+ // resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
419
+ out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
420
+ }
421
+ return out;
422
+ }
370
423
  /** The run's billing, from `system/init.apiKeySource`: claude.ai OAuth reports `"none"`, an
371
424
  * environment key reports `ANTHROPIC_API_KEY`, a helper reports `apiKeyHelper`. A
372
425
  * third-party provider overrides both — it is API billing even when the init omitted a
@@ -84,6 +84,15 @@ export function parseCodexLoginStatus(stdout) {
84
84
  return "api";
85
85
  return null;
86
86
  }
87
+ /** The refusal message when a codex seam is API-key logged in (decision 5): Codex reports no
88
+ * cost, so per-token billing would spend money the budget cap cannot see. The one home of the
89
+ * remedy text the startup gate, the live config editor, and the doctor all print, so the remedy
90
+ * command and its wording cannot drift between them. `home` is the codex home named in the
91
+ * remedy: the literal `<home>` placeholder where the call site does not resolve the home, else
92
+ * the resolved `CODEX_HOME` path. */
93
+ export function codexApiKeyRefusalMessage(home) {
94
+ return `Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run \`CODEX_HOME=${home} codex login --device-auth\``;
95
+ }
87
96
  /** The billing the login probe names, or null when it cannot be decided. */
88
97
  function seedBilling(config) {
89
98
  const status = probeLoginStatus(config);
@@ -1,7 +1,7 @@
1
1
  /** The codex adapter for the agent seam (plans/codex-backend.md §4, §5, §6): it shapes one
2
2
  * run's argv/stdin/env, translates codex's `exec --json` stream, reads the authoritative
3
3
  * rollout after the process exits, and classifies the exit's stderr — everything
4
- * backend-specific. src/agent/run-spawn.ts spawns the run and records this run in the session
4
+ * backend-specific. src/agent/agent-spawn.ts spawns the run and records this run in the session
5
5
  * ledger; codex mints its own session id (a thread id, or the resumed record's id) in the
6
6
  * stream's `thread.started`, which the runner's stream-mint pass appends to the ledger and
7
7
  * `invocation` returns so a later `exec resume` can reach it.
@@ -12,7 +12,7 @@
12
12
  * thread and the existing `handoff` path takes over (design §4). Together with the `resumable`
13
13
  * hook, that is every resume point's one rule.
14
14
  *
15
- * Registration lives in src/agent/run-agent.ts beside the pi, claude and opencode adapters; a
15
+ * Registration lives in src/agent/agent-run.ts beside the pi, claude and opencode adapters; a
16
16
  * test selects codex by setting `config.agent = "codex"` on a run. A run carrying a note gets
17
17
  * the notes path in its environment (the `tumwater-notes` command on PATH reads it), as the
18
18
  * pi and claude adapters do. */
@@ -1,18 +1,31 @@
1
+ /** Building the environment for one codex run (plans/codex-backend.md "The invocation" §3,
2
+ * findings 10, 13) — pure scrub-and-set over a base environment, with no subprocess. The caller
3
+ * layers the run-marker, the macOS LaunchServices preload and the notes shim on first, then hands
4
+ * the result here.
5
+ *
6
+ * Inherited state from a host Codex or Claude Code session breaks the team run: a Codex child
7
+ * keeps every `CODEX_*` it was given (finding 13), and `--ignore-user-config` still reads a host
8
+ * home whose `config.toml` a run writes into (finding 10). So every `CODEX_*`, the host Claude
9
+ * session's `CLAUDECODE` / `CLAUDE_CODE_*`, and both API keys an operator might carry
10
+ * (`OPENAI_API_KEY`, `CODEX_API_KEY`) are scrubbed — pi's OpenAI key must never switch Codex to
11
+ * per-token billing behind tumwater's back (decision 5). Everything else survives, including the
12
+ * run marker and the PATH that carries the notes shim.
13
+ *
14
+ * The set half points Codex at a home of the team's own (`CODEX_HOME`) when one is configured, so
15
+ * team sessions and trust entries stay out of the operator's `~/.codex` (finding 10). Unset, runs
16
+ * keep the operator's home. */
17
+ import { scrubEnv } from "../env-scrub.js";
1
18
  /** Build the codex process environment from `base`. Never mutates `base`. */
2
19
  export function codexEnv(base, opts = {}) {
3
- const env = {};
4
- for (const [key, value] of Object.entries(base)) {
5
- if (key === "CLAUDECODE")
6
- continue;
7
- if (key.startsWith("CODEX_"))
8
- continue;
9
- if (key.startsWith("CLAUDE_CODE_"))
10
- continue;
11
- if (key === "OPENAI_API_KEY" || key === "CODEX_API_KEY")
12
- continue;
13
- env[key] = value;
14
- }
15
- if (opts.home)
16
- env.CODEX_HOME = opts.home;
17
- return env;
20
+ return scrubEnv(base, {
21
+ keep: (key) => key !== "CLAUDECODE" &&
22
+ !key.startsWith("CODEX_") &&
23
+ !key.startsWith("CLAUDE_CODE_") &&
24
+ key !== "OPENAI_API_KEY" &&
25
+ key !== "CODEX_API_KEY",
26
+ then: (env) => {
27
+ if (opts.home)
28
+ env.CODEX_HOME = opts.home;
29
+ },
30
+ });
18
31
  }
@@ -37,29 +37,48 @@ function hookListMessages(cwd) {
37
37
  * `eventName: "preToolUse"` and `command` exactly the guard definition's (finding 3), or null
38
38
  * when none matches. The key and hash are returned as codex reports them. */
39
39
  function findTrust(result, definition) {
40
+ for (const group of hookGroups(result)) {
41
+ for (const hook of groupHooks(group)) {
42
+ const trust = matchHookTrust(hook, definition);
43
+ if (trust !== null)
44
+ return trust;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+ /** The top-level group list from `hooks/list`'s `result.data`, or `[]` when the result is not
50
+ * an object or has no array `data`. */
51
+ function hookGroups(result) {
40
52
  if (typeof result !== "object" || result === null)
41
- return null;
53
+ return [];
42
54
  const data = result.data;
43
- if (!Array.isArray(data))
55
+ if (Array.isArray(data))
56
+ return data;
57
+ return [];
58
+ }
59
+ /** One group's hook list from its `hooks` field, or `[]` when the group is not an object or
60
+ * has no array `hooks`. */
61
+ function groupHooks(group) {
62
+ if (typeof group !== "object" || group === null)
63
+ return [];
64
+ const hooks = group.hooks;
65
+ if (Array.isArray(hooks))
66
+ return hooks;
67
+ return [];
68
+ }
69
+ /** The trust of one hook when it is the guard's — `source` `sessionFlags`, `eventName`
70
+ * `preToolUse`, `command` exactly the definition's, with string `key` and `currentHash` — or
71
+ * null. */
72
+ function matchHookTrust(hook, definition) {
73
+ if (typeof hook !== "object" || hook === null)
44
74
  return null;
45
- for (const group of data) {
46
- if (typeof group !== "object" || group === null)
47
- continue;
48
- const hooks = group.hooks;
49
- if (!Array.isArray(hooks))
50
- continue;
51
- for (const hook of hooks) {
52
- if (typeof hook !== "object" || hook === null)
53
- continue;
54
- const h = hook;
55
- if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
56
- continue;
57
- if (h.command !== definition.command)
58
- continue;
59
- if (typeof h.key === "string" && typeof h.currentHash === "string") {
60
- return { key: h.key, hash: h.currentHash };
61
- }
62
- }
75
+ const h = hook;
76
+ if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
77
+ return null;
78
+ if (h.command !== definition.command)
79
+ return null;
80
+ if (typeof h.key === "string" && typeof h.currentHash === "string") {
81
+ return { key: h.key, hash: h.currentHash };
63
82
  }
64
83
  return null;
65
84
  }