tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,46 +1,18 @@
1
+ // vocabulary: plain
1
2
  import { fail, say, sayJson } from "../cli/cli-output.js";
2
3
  import { durationLabel, failRivalShapes, flagValue, parseCountFlag, parseGrepFlag, parseRoleScope, parseSinceFlag } from "../cli/cli-args.js";
3
- import { displayWidth, padToWidth } from "../text/text-width.js";
4
- import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, HISTORY_RESULT_CLASSES, historyClass, readTickRows, readTickRowsSince, tickRowCells, tickRowLine, tickRowsMatchingGrep } from "./history-data.js";
4
+ import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, HISTORY_RESULT_FLAG_NAMES, historyClass, parseHistoryResultClass, readTickRows, readTickRowsSince, summarizeTickRows, tickRowsMatchingGrep } from "./history-data.js";
5
+ import { renderRow, renderSummary, widestWidths } from "./history-render.js";
5
6
  import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
6
7
  import { gotSuffix } from "../text/text.js";
7
- /** Each column's width from its unpadded cell. Text cells measure terminal display columns
8
- * (displayWidth), not UTF-16 code units: a loop name holding CJK or emoji renders two columns
9
- * per code point, and a code-unit padEnd lets that row's later columns drift right of its
10
- * ASCII neighbors. The tick cell measures before its "#" prefix is added (the prefix rides
11
- * outside the pad), and duration is ASCII, so both take plain length. */
12
- function widthsOf(cells) {
13
- return {
14
- loop: displayWidth(cells.loop),
15
- tick: cells.tick.length,
16
- result: displayWidth(cells.result),
17
- duration: cells.duration.length,
18
- usage: displayWidth(cells.usage),
19
- };
20
- }
21
- /** The per-column maxima over every row the table shows — the widths derive from the rows
22
- * actually shown, so a single-row table has no padding gap. */
23
- function widestWidths(rows) {
24
- const widths = { loop: 0, tick: 0, result: 0, duration: 0, usage: 0 };
25
- for (const row of rows) {
26
- const w = widthsOf(tickRowCells(row));
27
- widths.loop = Math.max(widths.loop, w.loop);
28
- widths.tick = Math.max(widths.tick, w.tick);
29
- widths.result = Math.max(widths.result, w.result);
30
- widths.duration = Math.max(widths.duration, w.duration);
31
- widths.usage = Math.max(widths.usage, w.usage);
32
- }
33
- return widths;
34
- }
35
- /** One aligned table line: fixed-width columns over the row set, then the free-text detail.
36
- * Every cell is padded to its column's width even when empty — usage is the one cell that can
37
- * be empty (a tick with neither tokens nor cost), and dropping it would pull the detail left,
38
- * misaligning that row against its neighbors; tickRowLine strips only the trailing pad. The
39
- * row's column order rides history-data.ts's tickRowLine, shared with the grep haystack so the
40
- * two cannot disagree about which field sits where. */
41
- function renderRow(row, widths) {
42
- return tickRowLine(row, (column, value) => padToWidth(value, widths[column]));
43
- }
8
+ import { typoSuffix } from "../text/suggest.js";
9
+ /** `tumwater history [--role <id>] [-n N]`: one row per completed tick, newest first. The
10
+ * observing half beside cmdLogs (log-commands.ts): read-only over the event log, stdout only —
11
+ * every datum rides the `run_end` events src/tick/tick-finalize.ts already writes, so this
12
+ * is a rendering of the existing record, not a new one. The rows themselves are collected
13
+ * by history-data.ts (beside src/report/report-data.ts and src/failure/failure-data.ts), and
14
+ * the aligned table/summary text they print is rendered by history-render.ts (beside
15
+ * src/report/report-render.ts); this module parses the args, drives the CLI, and prints. */
44
16
  /** The missing-pattern error cmdHistory prints for a valueless or empty `--grep`, exported so
45
17
  * cli.ts's rejectUnknownArgs spec for --grep can fail a trailing `history --grep` with the same
46
18
  * wording. Kept local to this module — log-commands.ts holds its own GREP_VALUE_ERROR — so the
@@ -57,10 +29,10 @@ export const HISTORY_RESULT_VALUE_ERROR = "history --result needs a value";
57
29
  * speak. `--grep` keeps rows whose rendered line matches, and `--result` narrows to one outcome
58
30
  * class (changes/none/problems, the dashboard History filter's buckets); both compose with
59
31
  * --role, -n, and --since. Read-only: stdout only, no state file created — a missing or empty
60
- * event log prints `no ticks yet` and exits 0. */
32
+ * event log prints `no runs yet` and exits 0. */
61
33
  export async function cmdHistory(root, args) {
62
34
  // `--grep <text>` filters the row set case-insensitively against the WYSIWYG row line (plus a
63
- // `tick_end` prefix, the same haystack rule logs --grep applies to its rendered lines), so
35
+ // `run_end` prefix, the same haystack rule logs --grep applies to its rendered lines), so
64
36
  // both observing views answer substring questions the same way. The flag scan itself — the
65
37
  // value lookup, the `rest` construction that keeps a flag-shaped pattern (`history --grep
66
38
  // --since` greps for the text "--since") from impersonating a rival flag, and the
@@ -101,37 +73,40 @@ export async function cmdHistory(root, args) {
101
73
  rows = tickRowsMatchingGrep(rows, grepPattern);
102
74
  // The set --grep admitted, saved so the empty-state wording blames the filter that actually
103
75
  // emptied it: if the result filter below removes rows grep matched, naming the pattern would
104
- // be false (`no ticks matching "widget"` when feature #7 does match). The GUI draws the same
76
+ // be false (`no runs matching "widget"` when feature #7 does match). The GUI draws the same
105
77
  // line between a search and its outcome filter.
106
78
  const afterGrep = rows.length;
107
79
  // `--result <class>` narrows to one outcome bucket, reading the same canonical historyClass
108
80
  // rule the dashboard's History filter applies (shared from history-data.ts, not copied). It
109
81
  // composes with --role, -n, --since, and --grep, so no rival-shape rule: a missing or empty
110
82
  // value fails with its own wording, and an unknown class names the accepted ones (the gotSuffix
111
- // shape the other flag validators use). Applied after --grep, over the same rows both the table
83
+ // shape the other flag validators use, plus a did-you-mean hint for a near-miss of a real
84
+ // result — the same typoSuffix treatment `logs --type` gives its codes). Applied after --grep,
85
+ // over the same rows both the table
112
86
  // and --json serialize.
113
87
  const resultRaw = flagValue(rest, "--result");
114
88
  let resultClass = null;
115
89
  if (resultRaw !== null) {
116
90
  if (resultRaw === undefined || resultRaw === "")
117
91
  fail(HISTORY_RESULT_VALUE_ERROR);
118
- if (!HISTORY_RESULT_CLASSES.includes(resultRaw))
119
- fail(`history --result must be one of ${HISTORY_RESULT_CLASSES.join(", ")}${gotSuffix(resultRaw)}`);
120
- resultClass = resultRaw;
92
+ resultClass = parseHistoryResultClass(resultRaw);
93
+ if (resultClass === null)
94
+ fail(`history --result must be one of ${Object.values(HISTORY_RESULT_FLAG_NAMES).join(", ")}${gotSuffix(resultRaw)}${typoSuffix(resultRaw, Object.values(HISTORY_RESULT_FLAG_NAMES))}`);
121
95
  rows = rows.filter((r) => historyClass(r.result) === resultClass);
122
96
  }
123
97
  // --json swaps the renderer for the collector's own payload, exactly as status --json and
124
98
  // report --json: the same rows the table prints, each with ts/tokens/costUsd kept raw. A
125
- // JSON document even when the log is empty ({"rows":[]} — never the prose `no ticks yet`,
99
+ // JSON document even when the log is empty ({"rows":[]} — never the prose `no runs yet`,
126
100
  // the report --json precedent: the flag's output must be parseable in every exit-0 case).
101
+ const summary = rest.includes("--summary") && rows.length > 0;
127
102
  if (rest.includes("--json")) {
128
- sayJson({ rows });
103
+ sayJson(summary ? summarizeTickRows(rows) : { rows });
129
104
  return;
130
105
  }
131
106
  if (rows.length === 0) {
132
- // A grep that matched nothing must not borrow the empty-log prose: `no ticks yet` and
133
- // `no ticks in 2h` are statements about the LOG and the WINDOW, and both are false when
134
- // rows were scanned and filtered out — the operator would read a quiet fleet where the
107
+ // A grep that matched nothing must not borrow the empty-log prose: `no runs yet` and
108
+ // `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
109
+ // rows were scanned and filtered out — the operator would read a quiet team where the
135
110
  // truth is `the ticks are there, none match`. The logs --grep rule (its `no events
136
111
  // matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
137
112
  // a bare {"rows":[]} document above, never prose in either form.
@@ -141,17 +116,21 @@ export async function cmdHistory(root, args) {
141
116
  // empty-log/window prose is the true one — the dashboard's History view draws the same
142
117
  // line. A grep that admitted nothing keeps its own wording regardless of --result.
143
118
  say(resultClass !== null && afterGrep > 0
144
- ? `no ticks with result "${resultClass}"`
119
+ ? `no runs with result "${resultClass}"`
145
120
  : grepPattern !== null
146
- ? `no ticks matching "${grepPattern}"`
121
+ ? `no runs matching "${grepPattern}"`
147
122
  : sinceMs === null
148
- ? "no ticks yet"
149
- : `no ticks in ${durationLabel(sinceMs)}`);
123
+ ? "no runs yet"
124
+ : `no runs in ${durationLabel(sinceMs)}`);
150
125
  return;
151
126
  }
152
- const widths = widestWidths(rows);
153
- say(rows.map((r) => renderRow(r, widths)).join("\n"));
154
- // A sparse window is never mistaken for a quiet fleet — but the note only ever rides rows
127
+ if (summary)
128
+ say(renderSummary(summarizeTickRows(rows)));
129
+ else {
130
+ const widths = widestWidths(rows);
131
+ say(rows.map((r) => renderRow(r, widths)).join("\n"));
132
+ }
133
+ // A sparse window is never mistaken for a quiet team — but the note only ever rides rows
155
134
  // (an empty window returned above), it never touches --json output, and it is the shared
156
135
  // SPARSE_WINDOW_NOTE wording: the oldest retained event being inside the window is exactly
157
136
  // the unproven case, whether the cause is rotation or idleness.
@@ -1,9 +1,11 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { removeQuiet, unlinkAllMissingTolerant } from "../files/files.js";
4
5
  import { isJsonObject } from "../files/json-object.js";
5
6
  import { roleInboxDir } from "../paths.js";
6
7
  import { errorMessage, isNonBlankString } from "../text/text.js";
8
+ import { typoSuffix } from "../text/suggest.js";
7
9
  import { agree, plural } from "../text/phrases.js";
8
10
  /** Images attached to queued prompts: a GUI drop or paste into the composer saves each image
9
11
  * beside the prompt's queue file (same stem, an image extension — the `.md` filter in
@@ -30,6 +32,18 @@ export function isPromptImageSibling(stem, entry) {
30
32
  return ((rest.startsWith(".") && PROMPT_IMAGE_EXTENSIONS.includes(rest.toLowerCase())) ||
31
33
  (/^-\d+\./.test(rest) && PROMPT_IMAGE_EXTENSIONS.includes(rest.slice(rest.indexOf(".")).toLowerCase())));
32
34
  }
35
+ /** Pick a file name with extension `ext` under `stem` that does not collide with any name
36
+ * already in `taken`, and record it: `<stem><ext>` when free, else the first
37
+ * `<stem>-<n><ext>` (n from 2 up) that is. The one home of the dedup-suffix rule
38
+ * savePromptImages and movePromptImages both use, so a same-stem sibling can never overwrite
39
+ * another. `taken` is updated with the chosen name. */
40
+ function uniqueStemName(stem, ext, taken) {
41
+ let name = `${stem}${ext}`;
42
+ for (let n = 2; taken.has(name); n++)
43
+ name = `${stem}-${n}${ext}`;
44
+ taken.add(name);
45
+ return name;
46
+ }
33
47
  /** Per-image decoded-size cap. Exported so a surface that holds a file before reading it
34
48
  * (the GUI composer's drop/paste handler) can refuse an oversized image with the same bound
35
49
  * the endpoint enforces, instead of offering a chip the server later rejects. */
@@ -37,6 +51,11 @@ export const PROMPT_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
37
51
  /** Per-prompt image count cap. */
38
52
  export const PROMPT_IMAGES_MAX_COUNT = 4;
39
53
  const IMAGE_REF_LINE = /^\[image attached: (.+)\]$/;
54
+ /** The path a prompt line references, or undefined when the line (trimmed) is not exactly the
55
+ * `[image attached: …]` shape imageReferenceLines writes. */
56
+ function referencedImagePath(line) {
57
+ return IMAGE_REF_LINE.exec(line.trim())?.[1];
58
+ }
40
59
  const BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
41
60
  /** The image-count rule: at most PROMPT_IMAGES_MAX_COUNT entries. Split out of
42
61
  * promptImagesProblem so a surface holding the raw path list can refuse an over-long
@@ -54,7 +73,7 @@ export function promptImageExtensionProblem(name) {
54
73
  const ext = path.extname(name).toLowerCase();
55
74
  if (PROMPT_IMAGE_EXTENSIONS.includes(ext))
56
75
  return null;
57
- return `unsupported image type ${JSON.stringify(ext === "" ? name : `*${ext}`)} — expected one of ${PROMPT_IMAGE_EXTENSIONS.join(" ")}`;
76
+ return `unsupported image type ${JSON.stringify(ext === "" ? name : `*${ext}`)} — expected one of ${PROMPT_IMAGE_EXTENSIONS.join(" ")}${typoSuffix(ext, PROMPT_IMAGE_EXTENSIONS)}`;
58
77
  }
59
78
  /** The per-image size rule, in decoded bytes: at most PROMPT_IMAGE_MAX_BYTES. Split out so a
60
79
  * surface holding an on-disk path can stat it and refuse an oversized file before reading it
@@ -99,6 +118,54 @@ export function promptImagesProblem(images) {
99
118
  }
100
119
  return null;
101
120
  }
121
+ /** Read and validate an on-disk `--attach` path list into the GUI's image input shape — the
122
+ * throwing core the CLI's `readPromptImages` and the TUI's prompt-line image attach share, so
123
+ * both surfaces validate from one home. It applies the same cheap path-only rules first — the
124
+ * shared count cap before any file is read (so a glob that expanded past it is rejected
125
+ * without reading or base64-encoding dozens of files), then each path's extension,
126
+ * regular-file shape, and on-disk size (the regular-file check keeps this from reading a FIFO
127
+ * or device node that would block the caller indefinitely); a path that cannot be statted is
128
+ * left for the read below to report with its own missing-file wording. It reads each
129
+ * surviving file whole and re-runs the endpoint's validation (promptImagesProblem) so the
130
+ * error names the broken rule with the same wording the dashboard answers 400 with. Each
131
+ * failure is a thrown Error carrying that existing wording. */
132
+ export function readPromptImageFiles(paths) {
133
+ const countProblem = promptImagesCountProblem(paths.length);
134
+ if (countProblem !== null)
135
+ throw new Error(countProblem);
136
+ for (const p of paths) {
137
+ const extProblem = promptImageExtensionProblem(p);
138
+ if (extProblem !== null)
139
+ throw new Error(extProblem);
140
+ let stat;
141
+ try {
142
+ stat = fs.statSync(p);
143
+ }
144
+ catch {
145
+ // Missing or unreadable: the read below names it.
146
+ continue;
147
+ }
148
+ if (!stat.isFile())
149
+ throw new Error(`attached image ${JSON.stringify(p)} is not a regular file — pass a path to an image file`);
150
+ const sizeProblem = promptImageSizeProblem(path.basename(p), stat.size);
151
+ if (sizeProblem !== null)
152
+ throw new Error(sizeProblem);
153
+ }
154
+ const images = paths.map((p) => {
155
+ let bytes;
156
+ try {
157
+ bytes = fs.readFileSync(p);
158
+ }
159
+ catch (err) {
160
+ throw new Error(`cannot read attached image ${JSON.stringify(p)}: ${errorMessage(err)}`);
161
+ }
162
+ return { name: path.basename(p), dataBase64: bytes.toString("base64") };
163
+ });
164
+ const problem = promptImagesProblem(images);
165
+ if (problem !== null)
166
+ throw new Error(problem);
167
+ return images;
168
+ }
102
169
  /** Save already-validated images beside a queued prompt's queue file — one file per image,
103
170
  * named with the queue file's stem plus the image's extension (`<stem>.png`); same-extension
104
171
  * images after the first get a `-<n>` suffix so they cannot overwrite each other. The client's
@@ -137,10 +204,7 @@ export function savePromptImages(root, role, queueFile, images) {
137
204
  try {
138
205
  for (const image of images) {
139
206
  const ext = path.extname(image.name).toLowerCase();
140
- let name = `${stem}${ext}`;
141
- for (let n = 2; taken.has(name); n++)
142
- name = `${stem}-${n}${ext}`;
143
- taken.add(name);
207
+ const name = uniqueStemName(stem, ext, taken);
144
208
  const file = path.join(dir, name);
145
209
  inFlight = file;
146
210
  fs.writeFileSync(file, Buffer.from(image.dataBase64, "base64"));
@@ -178,7 +242,7 @@ export function imageReferenceLines(paths) {
178
242
  export function imageReferencePaths(text) {
179
243
  const paths = [];
180
244
  for (const line of text.split("\n")) {
181
- const referenced = IMAGE_REF_LINE.exec(line.trim())?.[1];
245
+ const referenced = referencedImagePath(line);
182
246
  if (referenced !== undefined)
183
247
  paths.push(referenced);
184
248
  }
@@ -214,7 +278,7 @@ export function movePromptImages(fromFile, toFile, text) {
214
278
  const problems = [];
215
279
  const out = [];
216
280
  for (const line of text.split("\n")) {
217
- const referenced = IMAGE_REF_LINE.exec(line.trim())?.[1];
281
+ const referenced = referencedImagePath(line);
218
282
  if (referenced === undefined) {
219
283
  out.push(line);
220
284
  continue;
@@ -230,10 +294,7 @@ export function movePromptImages(fromFile, toFile, text) {
230
294
  continue;
231
295
  }
232
296
  const ext = path.extname(base).toLowerCase();
233
- let name = `${toStem}${ext}`;
234
- for (let n = 2; taken.has(name); n++)
235
- name = `${toStem}-${n}${ext}`;
236
- taken.add(name);
297
+ const name = uniqueStemName(toStem, ext, taken);
237
298
  const target = path.join(toDir, name);
238
299
  try {
239
300
  fs.renameSync(resolved, target);
@@ -255,7 +316,7 @@ export function movePromptImages(fromFile, toFile, text) {
255
316
  * the writer's shape is content and stays. The reader-side counterpart to
256
317
  * imageReferencePaths: the same line predicate, applied to drop rather than collect. */
257
318
  export function stripImageReferenceLines(text) {
258
- const kept = text.split("\n").filter((line) => IMAGE_REF_LINE.exec(line.trim()) === null);
319
+ const kept = text.split("\n").filter((line) => referencedImagePath(line) === undefined);
259
320
  while (kept.length > 0 && kept[kept.length - 1].trim() === "")
260
321
  kept.pop();
261
322
  return kept.join("\n");
@@ -270,7 +331,7 @@ export function stripVanishedImageReferences(text) {
270
331
  const kept = [];
271
332
  let dropped = 0;
272
333
  for (const line of text.split("\n")) {
273
- const referenced = IMAGE_REF_LINE.exec(line.trim())?.[1];
334
+ const referenced = referencedImagePath(line);
274
335
  if (referenced !== undefined && !fs.existsSync(referenced)) {
275
336
  dropped++;
276
337
  continue;
@@ -281,6 +342,6 @@ export function stripVanishedImageReferences(text) {
281
342
  return { text, dropped: 0 };
282
343
  const body = kept.join("\n").replace(/\n+$/, "");
283
344
  const note = `[image attachments dropped: ${plural(dropped, "image file")} ${agree(dropped, "was", "were")}` +
284
- ` missing when this prompt was re-queued and ${agree(dropped, "is", "are")} no longer on disk]`;
345
+ ` missing when this prompt was returned to the inbox and ${agree(dropped, "is", "are")} no longer on disk]`;
285
346
  return { text: `${body}\n\n${note}`, dropped };
286
347
  }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import path from "node:path";
2
3
  import { logEventBestEffort } from "../events/events.js";
3
4
  import { roleInboxDir } from "../paths.js";
@@ -21,7 +22,7 @@ function takeCancelledPrompt(root, role, file) {
21
22
  // A cancelled prompt's image attachments go with it; a dequeue leaves them for the tick to
22
23
  // read and they are removed on fulfillment (src/inbox/pending-prompt.ts).
23
24
  removeSameStemSiblings(root, role, file);
24
- logEventBestEffort(root, { loop: role, type: "prompt_cancelled", preview: promptPreview(text) });
25
+ logEventBestEffort(root, { source: role, type: "prompt_cancelled", preview: promptPreview(text) });
25
26
  return { status: "cancelled", text };
26
27
  }
27
28
  /** Remove the Nth prompt queued for one loop — 1-based, as shown by `tumwater prompt --list` —
@@ -90,13 +91,14 @@ export function queueFileNameProblem(name) {
90
91
  return `file must be a plain queue-file basename${gotSuffix(name)} — send just the name, not a path`;
91
92
  }
92
93
  if (!name.endsWith(".md"))
93
- return `file must name a queued prompt's .md file${gotSuffix(name)}`;
94
+ return `file must name an inbox message's .md file${gotSuffix(name)}`;
94
95
  return null;
95
96
  }
96
97
  /** The file-addressed queue functions' shared entry guard: throw the reason a name fails
97
- * queueFileNameProblem, so cancelQueuedFile, editQueuedFile, and readQueuedPromptBody all
98
- * fail closed with the same error before touching disk. The callers run the guard anyway;
99
- * naming it in one place keeps the throw contract from drifting between them. */
98
+ * queueFileNameProblem, so cancelQueuedFile, editQueuedFile, readQueuedPromptBody, and
99
+ * topQueuedFile all fail closed with the same error before touching disk. The callers run
100
+ * the guard anyway; naming it in one place keeps the throw contract from drifting between
101
+ * them. */
100
102
  export function assertQueueFileName(name) {
101
103
  const problem = queueFileNameProblem(name);
102
104
  if (problem)
@@ -93,7 +93,7 @@ function editPromptAtFile(root, role, file, newText, images) {
93
93
  throw err;
94
94
  }
95
95
  const oldText = stripNotBeforeMarker(oldFileText);
96
- logEventBestEffort(root, { loop: role, type: "prompt_edited", preview: promptPreview(composed) });
96
+ logEventBestEffort(root, { source: role, type: "prompt_edited", preview: promptPreview(composed) });
97
97
  return { status: "edited", oldText, newText: composed };
98
98
  }
99
99
  /** Rewrite the Nth prompt queued for one loop — 1-based, as shown by `tumwater prompt
@@ -1,10 +1,15 @@
1
+ // vocabulary: plain
1
2
  import { enqueueRolePrompt, promptPreview } from "./inbox.js";
2
3
  import { unlinkAllMissingTolerant } from "../files/files.js";
3
4
  import { imageReferenceLines, promptImagesProblem, savePromptImages, } from "./inbox-attachments.js";
4
5
  import { logEventBestEffort } from "../events/events.js";
5
- import { INITIAL_PROMPT_MAX_CHARS } from "../brief.js";
6
+ import { INITIAL_PROMPT_MAX_CHARS } from "../brief/brief.js";
6
7
  import { DIRECTOR_ROLE } from "../roles/roles.js";
7
8
  import { tooLongMessage } from "../text/text.js";
9
+ import { defaultConfig, loadConfigSafe } from "../config/config.js";
10
+ import { roleImageInfo } from "../agent/image-input.js";
11
+ import { readImageProbeCache } from "../doctor/image-probe.js";
12
+ import { roleLabel } from "../roles/role-labels.js";
8
13
  /** The user-facing submission pipeline for the prompt queues (the store's mechanics —
9
14
  * listing, peeking, dequeuing, cancelling, and their race policy — live in src/inbox/inbox.ts,
10
15
  * and the image side of a submission in src/inbox/inbox-attachments.ts): the shared length cap,
@@ -26,7 +31,35 @@ export function promptLengthProblem(text, role = DIRECTOR_ROLE) {
26
31
  const prompt = text.trim();
27
32
  if (prompt.length <= DIRECTOR_PROMPT_MAX_CHARS)
28
33
  return null;
29
- return tooLongMessage("the prompt", prompt.length, DIRECTOR_PROMPT_MAX_CHARS, `it rides into the ${role} tick's prefill`);
34
+ return tooLongMessage("the prompt", prompt.length, DIRECTOR_PROMPT_MAX_CHARS, `it rides into the ${roleLabel(role)} run's prefill`);
35
+ }
36
+ /** The image-input submit guidance for one loop (plans/image-input.md "Surfaces" and
37
+ * "Attachments", part 5/5): what an image-carrying prompt to `role` means. `refusal` is the
38
+ * throw-and-submit-nothing message when the loop's seam reads no images and no peer tier can
39
+ * (an attachment no configured model can read is refused at submit, not discovered at tick
40
+ * time); `notice` is what a surface prints/shows when the prompt will instead rerun on another
41
+ * tier. Both null when the loop's own model reads images (or capability is unknown — an
42
+ * unknown seam is never assumed incapable, mirroring the doctor's `tier models` stance), so
43
+ * the common text-only or vision path says nothing. One resolver (roleImageInfo), shared with
44
+ * the status rows and role page, so the gate cannot disagree with what `tumwater role` or the
45
+ * images alert claim. */
46
+ export function imagePromptGuidance(root, role) {
47
+ const { config } = loadConfigSafe(root);
48
+ const cfg = config ?? defaultConfig();
49
+ // The same probe cache the status rows, role page, and images alert read (roleImageInfo's
50
+ // fourth argument): a claimed pi model whose doctor probe failed reads text-only here too,
51
+ // so the gate never grants image capability that the surfaces report as absent.
52
+ const { imageInput, imageReroute } = roleImageInfo(cfg, role, undefined, readImageProbeCache(root));
53
+ if (imageInput !== false)
54
+ return {};
55
+ if (imageReroute) {
56
+ return {
57
+ notice: `${role}'s model can't read images; this prompt runs on ${imageReroute}`,
58
+ };
59
+ }
60
+ return {
61
+ refusal: `${role}'s model can't read images and no configured model reads images for it — declare image input on a vision model in pi's models.json, or point a tier at one`,
62
+ };
30
63
  }
31
64
  /** A user submits a new prompt for one loop (TUI, GUI, or CLI): enqueue it there and record it
32
65
  * in the event log under that loop, best-effort — the queue write is the durable effect the
@@ -52,11 +85,18 @@ export function submitRolePrompt(root, role, text, images, notBeforeMs) {
52
85
  const imageProblem = promptImagesProblem(images);
53
86
  if (imageProblem)
54
87
  throw new Error(imageProblem);
88
+ // Image input gate (plans/image-input.md "Attachments", part 5/5): an attachment no
89
+ // configured model can read is refused here, before anything is queued or written — the
90
+ // tick would only fail discovering it. A loop that reroutes a blind read is not refused;
91
+ // the surface prints/shows the notice instead (imagePromptGuidance is called again there).
92
+ const guidance = imagePromptGuidance(root, role);
93
+ if (guidance.refusal)
94
+ throw new Error(guidance.refusal);
55
95
  return submitPromptWithImages(root, role, text, images, notBeforeMs);
56
96
  }
57
97
  const prompt = text.trim();
58
98
  enqueueRolePrompt(root, role, prompt, notBeforeMs);
59
- logEventBestEffort(root, { loop: role, type: "prompt_enqueued", preview: promptPreview(prompt) });
99
+ logEventBestEffort(root, { source: role, type: "prompt_enqueued", preview: promptPreview(prompt) });
60
100
  return prompt;
61
101
  }
62
102
  /** submitRolePrompt's image-carrying path: save each image beside the queue file and queue the
@@ -93,7 +133,7 @@ function submitPromptWithImages(root, role, text, images, notBeforeMs) {
93
133
  unlinkAllMissingTolerant(savedImagePaths);
94
134
  throw err;
95
135
  }
96
- logEventBestEffort(root, { loop: role, type: "prompt_enqueued", preview: promptPreview(final) });
136
+ logEventBestEffort(root, { source: role, type: "prompt_enqueued", preview: promptPreview(final) });
97
137
  return final;
98
138
  }
99
139
  /** A user submits a new director prompt; see submitRolePrompt. The optional `notBeforeMs` is
@@ -0,0 +1,83 @@
1
+ // vocabulary: plain
2
+ import path from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
5
+ import { queueFileName, queueFileStamp, removeQueueFile } from "../files/file-queue.js";
6
+ import { readTextIfExists, writeTextAtomic } from "../files/files.js";
7
+ import { roleInboxDir } from "../paths.js";
8
+ import { assertQueueFileName } from "./inbox-cancel.js";
9
+ import { promptPreview, queuedFileAtPosition, queuedFiles } from "./inbox.js";
10
+ import { movePromptImages, stripVanishedImageReferences } from "./inbox-attachments.js";
11
+ /** The rename-and-event pairing behind both address forms, given a resolved queue file: one
12
+ * stamp below the queue's minimum carries the file to the head — its text (and its
13
+ * `tumwater:not-before` marker, which rides in the text) byte-for-byte to the new stem, its
14
+ * image attachments moving to that new stem too (movePromptImages, with
15
+ * stripVanishedImageReferences dropping any already-gone reference) — and one `prompt_topped`
16
+ * event is logged under the loop. Returns { status: "gone" } when the file disappears between
17
+ * the caller's listing and the read here (a concurrent pop is a normal race, never an
18
+ * error), sharing the store's takeQueuedFile convention. */
19
+ function moveFileToTop(root, role, file) {
20
+ const text = readTextIfExists(file);
21
+ if (text === null)
22
+ return { status: "gone" }; // Dequeued or cancelled mid-listing.
23
+ // One stamp below the queue's minimum makes the renamed file the oldest (first) entry; a
24
+ // queue with no parseable stamps falls back to now (it is before every hand-placed name).
25
+ const stamps = queuedFiles(root, role)
26
+ .map((f) => queueFileStamp(path.basename(f)))
27
+ .filter((s) => s !== null);
28
+ let stamp = (stamps.length > 0 ? Math.min(...stamps) : Date.now()) - 1;
29
+ const dir = path.dirname(file);
30
+ let newFile = path.join(dir, queueFileName(stamp, 0, ".md"));
31
+ // A concurrent writer may have just taken a name we computed — step one lower rather than
32
+ // overwrite it; the pid in queueFileName keeps two concurrent tops' names apart too.
33
+ while (existsSync(newFile)) {
34
+ stamp--;
35
+ newFile = path.join(dir, queueFileName(stamp, 0, ".md"));
36
+ }
37
+ // The attachments travel with the prompt to the new stem, so the fulfil/cancel sweep finds
38
+ // them; a move that could not carry one attachment is warned but never fails the reorder.
39
+ const moved = movePromptImages(file, newFile, text);
40
+ if (moved.problem !== null)
41
+ warnEventBestEffort(root, role, moved.problem);
42
+ const carried = stripVanishedImageReferences(moved.text).text;
43
+ writeTextAtomic(newFile, carried);
44
+ removeQueueFile(file);
45
+ logEventBestEffort(root, { source: role, type: "prompt_topped", preview: promptPreview(carried) });
46
+ return { status: "topped", preview: promptPreview(carried) };
47
+ }
48
+ /** Move the Nth prompt queued for one loop — 1-based, as shown by `tumwater prompt --list` —
49
+ * to the head of its queue, so the loop dequeues it next. Inbox order is the queue-file name
50
+ * (`queueFileName`'s timestamp stamp), so the only way to make one prompt next is to give it a
51
+ * stamp one below the queue's minimum; the file's text (and its `tumwater:not-before` marker,
52
+ * which rides in the text) is carried byte-for-byte to the new stem, its image attachments
53
+ * move to that new stem too (movePromptImages, with stripVanishedImageReferences dropping any
54
+ * already-gone reference), and one `prompt_topped` event is logged under the loop.
55
+ *
56
+ * Throws for out-of-range positions and for position 1 ("already first"), both with no side
57
+ * effects — the moveEntryToTop precedent. Returns { status: "gone" } when the file disappears
58
+ * between listing and rename instead of throwing. The race policy and event live here,
59
+ * sharing the store's takeQueuedFile convention: a vanished file is a normal race, never an
60
+ * error. */
61
+ export function movePromptToTop(root, role, position) {
62
+ const file = queuedFileAtPosition(root, role, position);
63
+ if (position === 1)
64
+ throw new Error(`prompt ${position} is already first`);
65
+ return moveFileToTop(root, role, file);
66
+ }
67
+ /** Move one queued prompt addressed by its queue-file basename to the head of its role's
68
+ * queue — the file-addressed twin of movePromptToTop, for the dashboard's per-row Move to
69
+ * top affordance: the address is the file itself, so a stale poll can never reorder the
70
+ * wrong prompt. Throws for a name failing queueFileNameProblem (re-checked through
71
+ * assertQueueFileName, so a caller that skips the guard fails closed); a file the loop
72
+ * already dequeued answers "gone", and the queue head answers "already_first" with no write.
73
+ * Reuses moveFileToTop's rename-and-event pairing, so both address forms cannot drift. */
74
+ export function topQueuedFile(root, role, name) {
75
+ assertQueueFileName(name);
76
+ const target = path.join(roleInboxDir(root, role), name);
77
+ const index = queuedFiles(root, role).indexOf(target);
78
+ if (index === -1)
79
+ return { status: "gone" };
80
+ if (index === 0)
81
+ return { status: "already_first" };
82
+ return moveFileToTop(root, role, target);
83
+ }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { ensureParentDir, readTextIfExists, writeTextAtomic } from "../files/files.js";
@@ -19,7 +20,9 @@ import { isPromptImageSibling } from "./inbox-attachments.js";
19
20
  * takeQueuedFile race policy. The cancel half — position-, list-, and file-addressed removal
20
21
  * and its event pairing — lives in inbox-cancel.ts; the in-place edit of one queued
21
22
  * prompt's text (`prompt --edit`, position-addressed like cancel and sharing its
22
- * resolveListedQueue) lives in inbox-edit.ts; the submission pipeline that validates and
23
+ * resolveListedQueue) lives in inbox-edit.ts; moving one queued prompt to its queue's
24
+ * head (`prompt --top`, position-addressed like cancel) lives in inbox-top.ts; the
25
+ * submission pipeline that validates and
23
26
  * logs a user-submitted prompt lives in inbox-submit.ts; the image side of a submission in
24
27
  * inbox-attachments.ts. The not-before marker vocabulary for deferred prompts lives in
25
28
  * prompt-not-before.ts (compose, parse, strip, and the deliverableNow predicate this module
@@ -84,7 +87,10 @@ export function queuedFileAtPosition(root, role, position) {
84
87
  const files = queuedFiles(root, role);
85
88
  const file = files[position - 1];
86
89
  if (position < 1 || position > files.length || !file) {
87
- throw new Error(`no prompt at position ${position} (${files.length} queued)`);
90
+ // Name the valid range when the queue is non-empty, so the operator sees the fix in the
91
+ // same line instead of deriving `1..N` from the count; an empty queue needs no range.
92
+ const range = files.length > 0 ? ` — valid: 1-${files.length}` : "";
93
+ throw new Error(`no prompt at position ${position} (${files.length} in the inbox)${range}`);
88
94
  }
89
95
  return file;
90
96
  }
@@ -140,7 +140,7 @@ export class PendingPrompt {
140
140
  return stripVanishedImageReferences(userPrompt).text;
141
141
  const moved = movePromptImages(fromFile, newFile, userPrompt);
142
142
  if (moved.problem !== null) {
143
- warnEventBestEffort(this.root, this.role, `could not move every re-queued prompt attachment: ${moved.problem}`);
143
+ warnEventBestEffort(this.root, this.role, `could not move every attachment of the prompt returned to the inbox: ${moved.problem}`);
144
144
  }
145
145
  return stripVanishedImageReferences(moved.text).text;
146
146
  });