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.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- 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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
// `
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
133
|
-
// `no
|
|
134
|
-
// rows were scanned and filtered out — the operator would read a quiet
|
|
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
|
|
119
|
+
? `no runs with result "${resultClass}"`
|
|
145
120
|
: grepPattern !== null
|
|
146
|
-
? `no
|
|
121
|
+
? `no runs matching "${grepPattern}"`
|
|
147
122
|
: sinceMs === null
|
|
148
|
-
? "no
|
|
149
|
-
: `no
|
|
123
|
+
? "no runs yet"
|
|
124
|
+
: `no runs in ${durationLabel(sinceMs)}`);
|
|
150
125
|
return;
|
|
151
126
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
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, {
|
|
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
|
|
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,
|
|
98
|
-
* fail closed with the same error before touching disk. The callers run
|
|
99
|
-
* naming it in one place keeps the throw contract from drifting between
|
|
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, {
|
|
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}
|
|
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, {
|
|
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, {
|
|
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
|
+
}
|
package/dist/src/inbox/inbox.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
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
|
});
|