tumwater 0.1.3 → 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 (359) hide show
  1. package/README.md +63 -28
  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-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,61 +1,18 @@
1
- import { formatTimestamp } from "../text/datetime.js";
1
+ // vocabulary: plain
2
2
  import { fail, say, sayJson } from "../cli/cli-output.js";
3
3
  import { durationLabel, failRivalShapes, flagValue, parseCountFlag, parseGrepFlag, parseRoleScope, parseSinceFlag } from "../cli/cli-args.js";
4
- import { displayWidth, padToWidth } from "../text/text-width.js";
5
- import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, HISTORY_RESULT_CLASSES, historyClass, readTickRows, readTickRowsSince, summarizeTickRows, 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";
6
6
  import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
7
7
  import { gotSuffix } from "../text/text.js";
8
- /** Each column's width from its unpadded cell. Text cells measure terminal display columns
9
- * (displayWidth), not UTF-16 code units: a loop name holding CJK or emoji renders two columns
10
- * per code point, and a code-unit padEnd lets that row's later columns drift right of its
11
- * ASCII neighbors. The tick cell measures before its "#" prefix is added (the prefix rides
12
- * outside the pad), and duration is ASCII, so both take plain length. */
13
- function widthsOf(cells) {
14
- return {
15
- loop: displayWidth(cells.loop),
16
- tick: cells.tick.length,
17
- result: displayWidth(cells.result),
18
- duration: cells.duration.length,
19
- usage: displayWidth(cells.usage),
20
- };
21
- }
22
- /** The per-column maxima over every row the table shows — the widths derive from the rows
23
- * actually shown, so a single-row table has no padding gap. */
24
- function widestWidths(rows) {
25
- const widths = { loop: 0, tick: 0, result: 0, duration: 0, usage: 0 };
26
- for (const row of rows) {
27
- const w = widthsOf(tickRowCells(row));
28
- widths.loop = Math.max(widths.loop, w.loop);
29
- widths.tick = Math.max(widths.tick, w.tick);
30
- widths.result = Math.max(widths.result, w.result);
31
- widths.duration = Math.max(widths.duration, w.duration);
32
- widths.usage = Math.max(widths.usage, w.usage);
33
- }
34
- return widths;
35
- }
36
- /** One aligned table line: fixed-width columns over the row set, then the free-text detail.
37
- * Every cell is padded to its column's width even when empty — usage is the one cell that can
38
- * be empty (a tick with neither tokens nor cost), and dropping it would pull the detail left,
39
- * misaligning that row against its neighbors; tickRowLine strips only the trailing pad. The
40
- * row's column order rides history-data.ts's tickRowLine, shared with the grep haystack so the
41
- * two cannot disagree about which field sits where. */
42
- function renderRow(row, widths) {
43
- return tickRowLine(row, (column, value) => padToWidth(value, widths[column]));
44
- }
45
- /** The `--summary` text: a header, one line per loop, then the total line. The loop column pads to
46
- * display width; each number column right-aligns under its header; the last column, `last change`,
47
- * is the newest change's timestamp (`—` when none) and is left-aligned, unpadded. */
48
- export function renderSummary(summary) {
49
- const all = [...summary.loops, summary.total];
50
- const loopWidth = Math.max(...["loop", ...all.map((r) => r.loop)].map(displayWidth));
51
- const cols = ["ticks", "changes", "none", "problems"];
52
- const widths = cols.map((c) => Math.max(c.length, ...all.map((r) => String(r[c]).length)));
53
- const line = (loop, cells, lastChange) => [padToWidth(loop, loopWidth), ...cells.map((c, i) => c.padStart(widths[i] ?? 0)), lastChange].join(" ").trimEnd();
54
- return [
55
- line("loop", [...cols], "last change"),
56
- ...all.map((r) => line(r.loop, cols.map((c) => String(r[c])), r.lastChangeTs === null ? "—" : formatTimestamp(r.lastChangeTs))),
57
- ].join("\n");
58
- }
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. */
59
16
  /** The missing-pattern error cmdHistory prints for a valueless or empty `--grep`, exported so
60
17
  * cli.ts's rejectUnknownArgs spec for --grep can fail a trailing `history --grep` with the same
61
18
  * wording. Kept local to this module — log-commands.ts holds its own GREP_VALUE_ERROR — so the
@@ -72,10 +29,10 @@ export const HISTORY_RESULT_VALUE_ERROR = "history --result needs a value";
72
29
  * speak. `--grep` keeps rows whose rendered line matches, and `--result` narrows to one outcome
73
30
  * class (changes/none/problems, the dashboard History filter's buckets); both compose with
74
31
  * --role, -n, and --since. Read-only: stdout only, no state file created — a missing or empty
75
- * event log prints `no ticks yet` and exits 0. */
32
+ * event log prints `no runs yet` and exits 0. */
76
33
  export async function cmdHistory(root, args) {
77
34
  // `--grep <text>` filters the row set case-insensitively against the WYSIWYG row line (plus a
78
- // `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
79
36
  // both observing views answer substring questions the same way. The flag scan itself — the
80
37
  // value lookup, the `rest` construction that keeps a flag-shaped pattern (`history --grep
81
38
  // --since` greps for the text "--since") from impersonating a rival flag, and the
@@ -116,28 +73,30 @@ export async function cmdHistory(root, args) {
116
73
  rows = tickRowsMatchingGrep(rows, grepPattern);
117
74
  // The set --grep admitted, saved so the empty-state wording blames the filter that actually
118
75
  // emptied it: if the result filter below removes rows grep matched, naming the pattern would
119
- // 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
120
77
  // line between a search and its outcome filter.
121
78
  const afterGrep = rows.length;
122
79
  // `--result <class>` narrows to one outcome bucket, reading the same canonical historyClass
123
80
  // rule the dashboard's History filter applies (shared from history-data.ts, not copied). It
124
81
  // composes with --role, -n, --since, and --grep, so no rival-shape rule: a missing or empty
125
82
  // value fails with its own wording, and an unknown class names the accepted ones (the gotSuffix
126
- // 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
127
86
  // and --json serialize.
128
87
  const resultRaw = flagValue(rest, "--result");
129
88
  let resultClass = null;
130
89
  if (resultRaw !== null) {
131
90
  if (resultRaw === undefined || resultRaw === "")
132
91
  fail(HISTORY_RESULT_VALUE_ERROR);
133
- if (!HISTORY_RESULT_CLASSES.includes(resultRaw))
134
- fail(`history --result must be one of ${HISTORY_RESULT_CLASSES.join(", ")}${gotSuffix(resultRaw)}`);
135
- 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))}`);
136
95
  rows = rows.filter((r) => historyClass(r.result) === resultClass);
137
96
  }
138
97
  // --json swaps the renderer for the collector's own payload, exactly as status --json and
139
98
  // report --json: the same rows the table prints, each with ts/tokens/costUsd kept raw. A
140
- // 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`,
141
100
  // the report --json precedent: the flag's output must be parseable in every exit-0 case).
142
101
  const summary = rest.includes("--summary") && rows.length > 0;
143
102
  if (rest.includes("--json")) {
@@ -145,9 +104,9 @@ export async function cmdHistory(root, args) {
145
104
  return;
146
105
  }
147
106
  if (rows.length === 0) {
148
- // A grep that matched nothing must not borrow the empty-log prose: `no ticks yet` and
149
- // `no ticks in 2h` are statements about the LOG and the WINDOW, and both are false when
150
- // 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
151
110
  // truth is `the ticks are there, none match`. The logs --grep rule (its `no events
152
111
  // matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
153
112
  // a bare {"rows":[]} document above, never prose in either form.
@@ -157,12 +116,12 @@ export async function cmdHistory(root, args) {
157
116
  // empty-log/window prose is the true one — the dashboard's History view draws the same
158
117
  // line. A grep that admitted nothing keeps its own wording regardless of --result.
159
118
  say(resultClass !== null && afterGrep > 0
160
- ? `no ticks with result "${resultClass}"`
119
+ ? `no runs with result "${resultClass}"`
161
120
  : grepPattern !== null
162
- ? `no ticks matching "${grepPattern}"`
121
+ ? `no runs matching "${grepPattern}"`
163
122
  : sinceMs === null
164
- ? "no ticks yet"
165
- : `no ticks in ${durationLabel(sinceMs)}`);
123
+ ? "no runs yet"
124
+ : `no runs in ${durationLabel(sinceMs)}`);
166
125
  return;
167
126
  }
168
127
  if (summary)
@@ -171,7 +130,7 @@ export async function cmdHistory(root, args) {
171
130
  const widths = widestWidths(rows);
172
131
  say(rows.map((r) => renderRow(r, widths)).join("\n"));
173
132
  }
174
- // A sparse window is never mistaken for a quiet fleet — but the note only ever rides rows
133
+ // A sparse window is never mistaken for a quiet team — but the note only ever rides rows
175
134
  // (an empty window returned above), it never touches --json output, and it is the shared
176
135
  // SPARSE_WINDOW_NOTE wording: the oldest retained event being inside the window is exactly
177
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. */
@@ -59,7 +73,7 @@ export function promptImageExtensionProblem(name) {
59
73
  const ext = path.extname(name).toLowerCase();
60
74
  if (PROMPT_IMAGE_EXTENSIONS.includes(ext))
61
75
  return null;
62
- 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)}`;
63
77
  }
64
78
  /** The per-image size rule, in decoded bytes: at most PROMPT_IMAGE_MAX_BYTES. Split out so a
65
79
  * surface holding an on-disk path can stat it and refuse an oversized file before reading it
@@ -104,6 +118,54 @@ export function promptImagesProblem(images) {
104
118
  }
105
119
  return null;
106
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
+ }
107
169
  /** Save already-validated images beside a queued prompt's queue file — one file per image,
108
170
  * named with the queue file's stem plus the image's extension (`<stem>.png`); same-extension
109
171
  * images after the first get a `-<n>` suffix so they cannot overwrite each other. The client's
@@ -142,10 +204,7 @@ export function savePromptImages(root, role, queueFile, images) {
142
204
  try {
143
205
  for (const image of images) {
144
206
  const ext = path.extname(image.name).toLowerCase();
145
- let name = `${stem}${ext}`;
146
- for (let n = 2; taken.has(name); n++)
147
- name = `${stem}-${n}${ext}`;
148
- taken.add(name);
207
+ const name = uniqueStemName(stem, ext, taken);
149
208
  const file = path.join(dir, name);
150
209
  inFlight = file;
151
210
  fs.writeFileSync(file, Buffer.from(image.dataBase64, "base64"));
@@ -235,10 +294,7 @@ export function movePromptImages(fromFile, toFile, text) {
235
294
  continue;
236
295
  }
237
296
  const ext = path.extname(base).toLowerCase();
238
- let name = `${toStem}${ext}`;
239
- for (let n = 2; taken.has(name); n++)
240
- name = `${toStem}-${n}${ext}`;
241
- taken.add(name);
297
+ const name = uniqueStemName(toStem, ext, taken);
242
298
  const target = path.join(toDir, name);
243
299
  try {
244
300
  fs.renameSync(resolved, target);
@@ -286,6 +342,6 @@ export function stripVanishedImageReferences(text) {
286
342
  return { text, dropped: 0 };
287
343
  const body = kept.join("\n").replace(/\n+$/, "");
288
344
  const note = `[image attachments dropped: ${plural(dropped, "image file")} ${agree(dropped, "was", "were")}` +
289
- ` 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]`;
290
346
  return { text: `${body}\n\n${note}`, dropped };
291
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
  });