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.
- package/README.md +63 -28
- 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-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- 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 +178 -441
- 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 +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- 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 +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- 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 +13 -224
- package/dist/src/cli.js +78 -36
- 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 +107 -325
- 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 +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- 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 +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- 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 +80 -30
- 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 +6 -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 +79 -59
- 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 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- 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 +25 -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 +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- 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 +114 -73
- 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 +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -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 -20
- 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 +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- 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 +124 -38
- 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 +4 -3
- 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 +36 -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 +29 -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 +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- 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 +55 -34
- 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 +109 -131
- 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 +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- 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 +72 -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 +50 -39
- 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 +84 -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} +39 -38
- 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 +142 -25
- 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 +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- 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 +67 -53
- 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 +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- 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 +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -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 +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -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 +9 -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 +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- 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 +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- 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 -56
- package/dist/src/landing/landing-slot.js +0 -256
- 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,61 +1,18 @@
|
|
|
1
|
-
|
|
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 {
|
|
5
|
-
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";
|
|
6
6
|
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
7
7
|
import { gotSuffix } from "../text/text.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
// `
|
|
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
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
149
|
-
// `no
|
|
150
|
-
// 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
|
|
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
|
|
119
|
+
? `no runs with result "${resultClass}"`
|
|
161
120
|
: grepPattern !== null
|
|
162
|
-
? `no
|
|
121
|
+
? `no runs matching "${grepPattern}"`
|
|
163
122
|
: sinceMs === null
|
|
164
|
-
? "no
|
|
165
|
-
: `no
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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, {
|
|
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
|
});
|