tumwater 0.2.0 → 0.3.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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** The repo-readiness gate every repo-bound command runs before its body reads state: fail
|
|
2
2
|
* fast on the first unmet precondition (startup-gate.ts's repoNotReady — the repo half of
|
|
3
|
-
* `tumwater
|
|
3
|
+
* `tumwater start`'s startup gate). One named home beside the argument gate (cli-flag-specs.ts):
|
|
4
4
|
* the dispatcher (cli.ts), the marker commands (cli-marker-commands.ts), the write commands
|
|
5
5
|
* (backlog/backlog-cli.ts), and the read-only command bodies (cli-query-commands.ts) all
|
|
6
6
|
* import it, so a command layer never reaches into a sibling command module for a gate. */
|
|
7
7
|
import { fail } from "./cli-output.js";
|
|
8
|
+
import { teamWorkspaceMessage } from "../gates/readiness.js";
|
|
8
9
|
import { repoNotReady } from "../gates/startup-gate.js";
|
|
10
|
+
import { owningTeamRoot } from "../paths.js";
|
|
9
11
|
/** Fail (exit 1) on the first unmet repo precondition, naming the fix; return once the repo is
|
|
10
12
|
* ready. */
|
|
11
13
|
export async function requireReadyRepo(root) {
|
|
@@ -13,3 +15,11 @@ export async function requireReadyRepo(root) {
|
|
|
13
15
|
if (notReady !== null)
|
|
14
16
|
fail(notReady);
|
|
15
17
|
}
|
|
18
|
+
/** The workspace half of requireReadyRepo, for the commands that deliberately skip the
|
|
19
|
+
* ready-repo gate: inside a team workspace, fail (exit 1) naming the owning team root. Outside
|
|
20
|
+
* one it is a no-op, so those commands keep degrading to empty output in a plain directory. */
|
|
21
|
+
export function refuseTeamWorkspace(root) {
|
|
22
|
+
const owner = owningTeamRoot(root);
|
|
23
|
+
if (owner !== undefined)
|
|
24
|
+
fail(teamWorkspaceMessage(owner));
|
|
25
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/** `tumwater role <id>`'s command body, split out of cli-query-commands.ts so that module
|
|
2
|
+
* keeps only the read-only query commands (status, diff, queue, backlog). cli.ts's dispatcher
|
|
3
|
+
* routes to cmdRole here exactly as it routes cmdStatus to cli-query-commands or cmdConfig to
|
|
4
|
+
* config-commands. cmdRole is the one query-named command that also writes: it renders a
|
|
5
|
+
* role's standing and next-run prompts and carries ten per-role write flags (notes,
|
|
6
|
+
* instructions, quiet-hours, daily cap, min-run-interval, instances, enable/disable, and the
|
|
7
|
+
* provider/model/thinking triple), each through the same setConfigKey/unsetConfigKey core
|
|
8
|
+
* `config set`/`config unset` use. It lives in its own file because its mixed
|
|
9
|
+
* positional/flag vocabulary and its write branches are its own, not the query commands'. */
|
|
10
|
+
import { fail, say, sayJsonOrRender } from "./cli-output.js";
|
|
11
|
+
import { flagValue } from "./cli-args.js";
|
|
12
|
+
import { JSON_FLAG, ROLE_FLAG, rejectUnknownArgs } from "./cli-flag-specs.js";
|
|
13
|
+
import { refuseTeamWorkspace } from "./cli-repo-gate.js";
|
|
14
|
+
import { defaultConfig, knownRoleIds, loadConfigSafe } from "../config/config.js";
|
|
15
|
+
import { rolePayload } from "../roles/role-view.js";
|
|
16
|
+
import { renderRoleMarkdown } from "../roles/role-render.js";
|
|
17
|
+
import { DIRECTOR_ROLE, unknownRoleMessage } from "../roles/roles.js";
|
|
18
|
+
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
19
|
+
import { roleNotesPath } from "../paths.js";
|
|
20
|
+
import { errorMessage } from "../text/text.js";
|
|
21
|
+
import { readPromptFileArg } from "./cli-command-args.js";
|
|
22
|
+
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
23
|
+
import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
|
|
24
|
+
import { setConfigKey, unsetConfigKey } from "../config/config-write.js";
|
|
25
|
+
/** `role`'s notes-write flags: clear the note, or replace it from a file (`-` for stdin). */
|
|
26
|
+
const CLEAR_NOTES_SPEC = { names: ["--clear-notes"] };
|
|
27
|
+
const NOTES_FILE_SPEC = { names: ["--notes-file"], value: true, valueName: "<path>" };
|
|
28
|
+
/** `role`'s instructions-override flags: clear it, or replace it from a file (`-` for stdin). */
|
|
29
|
+
const CLEAR_INSTRUCTIONS_SPEC = { names: ["--clear-instructions"] };
|
|
30
|
+
const INSTRUCTIONS_FILE_SPEC = { names: ["--instructions-file"], value: true, valueName: "<path>" };
|
|
31
|
+
/** `role`'s quiet-hours write flags: clear the window, or set it to a daily local-time window. */
|
|
32
|
+
const CLEAR_QUIET_HOURS_SPEC = { names: ["--clear-quiet-hours"] };
|
|
33
|
+
const QUIET_HOURS_SPEC = { names: ["--quiet-hours"], value: true, valueName: "<window>" };
|
|
34
|
+
/** `role`'s daily-cap write flags: clear the cap, or set it to a USD amount (0 disables). */
|
|
35
|
+
const CLEAR_CAP_SPEC = { names: ["--clear-cap"] };
|
|
36
|
+
const CAP_SPEC = { names: ["--cap"], value: true, valueName: "<usd>" };
|
|
37
|
+
/** A plain-decimal count like `.5`, `05` or `00.5` that Number() reads but JSON.parse cannot
|
|
38
|
+
* (JSON forbids a leading dot and a leading zero), so a `role` numeric write must spell it back
|
|
39
|
+
* in JSON form (`0.5` for `00.5`) before setConfigKey, whose JSON.parse fallback would
|
|
40
|
+
* otherwise store the literal string. The same plain-decimal rule the TUI's and GUI's editors
|
|
41
|
+
* apply (`Number()`), so the terminal admits exactly the spellings they do. Returns null for
|
|
42
|
+
* text that is not a plain decimal, so the caller leaves it untouched (the config validator
|
|
43
|
+
* owns the rest). */
|
|
44
|
+
function plainDecimalToJson(raw) {
|
|
45
|
+
return /^\d*\.?\d+$/.test(raw) ? String(Number(raw)) : null;
|
|
46
|
+
}
|
|
47
|
+
/** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
|
|
48
|
+
* back-to-back). */
|
|
49
|
+
const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
|
|
50
|
+
const MIN_RUN_INTERVAL_SPEC = { names: ["--min-run-interval"], value: true, valueName: "<seconds>" };
|
|
51
|
+
/** `role`'s instances write flags: clear the count, or set it to a parallel-runner count
|
|
52
|
+
* (an integer from 1 to 8; only feature and bugfix split). */
|
|
53
|
+
const CLEAR_INSTANCES_SPEC = { names: ["--clear-instances"] };
|
|
54
|
+
const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" };
|
|
55
|
+
/** `role`'s enabled-state write flags: enable or disable the role, written to
|
|
56
|
+
* roles.<base>.enabled. */
|
|
57
|
+
const ENABLE_SPEC = { names: ["--enable"] };
|
|
58
|
+
const DISABLE_SPEC = { names: ["--disable"] };
|
|
59
|
+
/** `role`'s model-triple write flags: clear the field, or set it, written to
|
|
60
|
+
* roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
|
|
61
|
+
* pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
|
|
62
|
+
const CLEAR_PROVIDER_SPEC = { names: ["--clear-provider"] };
|
|
63
|
+
const PROVIDER_SPEC = { names: ["--provider"], value: true, valueName: "<id>" };
|
|
64
|
+
const CLEAR_MODEL_SPEC = { names: ["--clear-model"] };
|
|
65
|
+
const MODEL_SPEC = { names: ["--model"], value: true, valueName: "<id>" };
|
|
66
|
+
const CLEAR_THINKING_SPEC = { names: ["--clear-thinking"] };
|
|
67
|
+
const THINKING_SPEC = { names: ["--thinking"], value: true, valueName: "<level>" };
|
|
68
|
+
/** Peel `tumwater role <id>`'s positional id off the argument list: the first token that is
|
|
69
|
+
* neither a flag nor the value of a --role, --notes-file, --instructions-file,
|
|
70
|
+
* --quiet-hours, --cap, --min-run-interval, --instances, --provider, --model or
|
|
71
|
+
* --thinking flag. Returns the id (null when absent) and the remaining arguments — flags
|
|
72
|
+
* only, ready for rejectUnknownArgs and sayJsonOrRender. The id may also arrive as
|
|
73
|
+
* --role <id>, the flag spelling every other role-targeting command shares; the
|
|
74
|
+
* collector's caller rejects both spellings at once rather than silently picking one.
|
|
75
|
+
* Lives here because it exists only for this one
|
|
76
|
+
* command's mixed positional/flag vocabulary. */
|
|
77
|
+
function peelRolePositional(args) {
|
|
78
|
+
const valueIdx = new Set();
|
|
79
|
+
for (const flag of ["--role", "--notes-file", "--instructions-file", "--quiet-hours", "--cap", "--min-run-interval", "--instances", "--provider", "--model", "--thinking"]) {
|
|
80
|
+
const at = args.indexOf(flag);
|
|
81
|
+
if (at >= 0)
|
|
82
|
+
valueIdx.add(at + 1);
|
|
83
|
+
}
|
|
84
|
+
let id = null;
|
|
85
|
+
const rest = [];
|
|
86
|
+
for (let i = 0; i < args.length; i++) {
|
|
87
|
+
const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
|
|
88
|
+
// A non-flag token that is not a value-taking flag's value is the positional,
|
|
89
|
+
// taken once; everything else — flags and their values alike — rides in rest for
|
|
90
|
+
// rejectUnknownArgs and flagValue to claim.
|
|
91
|
+
if (!arg.startsWith("-") && id === null && !valueIdx.has(i)) {
|
|
92
|
+
id = arg;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
rest.push(arg);
|
|
96
|
+
}
|
|
97
|
+
return { id, rest };
|
|
98
|
+
}
|
|
99
|
+
/** Refuse an unknown role id with the shared unknownRoleMessage wording before a per-role
|
|
100
|
+
* config write resolves the id. Every `tumwater role` write branch starts with the same
|
|
101
|
+
* prelude — load the config (falling back to defaultConfig so built-in roles resolve) and
|
|
102
|
+
* throw the unknown-role error unless the id appears — so this keeps that check in one place.
|
|
103
|
+
* The caller then derives `baseRoleOf(id)` for the write key. */
|
|
104
|
+
function requireKnownRole(root, id) {
|
|
105
|
+
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
106
|
+
if (!knownRoleIds(cfg).includes(id))
|
|
107
|
+
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
108
|
+
}
|
|
109
|
+
/** The config-key write the six per-role hold branches (quiet hours, daily cap, min run
|
|
110
|
+
* interval, provider, model and thinking) share: with `clear`, or an empty typed value, unset
|
|
111
|
+
* `key` under `base`; else set it. `serialize` shapes the value `setConfigKey` stores (it
|
|
112
|
+
* JSON-encodes nothing itself) and `display` gives the human phrase for the confirmation
|
|
113
|
+
* line. The caller returns right after. The instructions branch keeps its own block: its
|
|
114
|
+
* value is a file's text, so an empty value is a legitimate write, not a clear. A no-op (a
|
|
115
|
+
* value the key already resolves to, or a clear of a key the file never held) reports itself
|
|
116
|
+
* and writes nothing. */
|
|
117
|
+
function writeRoleConfigKey(root, base, clear, raw, key, noun, serialize, display) {
|
|
118
|
+
if (clear || raw.trim() === "") {
|
|
119
|
+
const result = unsetConfigKey(root, key);
|
|
120
|
+
if (!result.ok)
|
|
121
|
+
fail(result.error);
|
|
122
|
+
say(result.oldValue === undefined
|
|
123
|
+
? `${base}: ${noun} is not set in tumwater.json, so nothing was cleared`
|
|
124
|
+
: `${base}: ${noun} cleared`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const result = setConfigKey(root, key, serialize(raw));
|
|
128
|
+
if (!result.ok)
|
|
129
|
+
fail(result.error);
|
|
130
|
+
say(result.unchanged
|
|
131
|
+
? `${base}: ${noun} is already ${display(raw)}, so tumwater.json was not written`
|
|
132
|
+
: `${base}: ${noun} set to ${display(raw)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** `tumwater role <id>`: one role's standing prompt and next run's assembled prompt. */
|
|
136
|
+
export async function cmdRole(root, args) {
|
|
137
|
+
// No requireReadyRepo gate, like backlog: role-view degrades (missing state files →
|
|
138
|
+
// fresh defaults, a missing queue directory an empty inbox), so the command inspects
|
|
139
|
+
// a repo the team never started in — and a torn one — instead of refusing (a team
|
|
140
|
+
// workspace still refuses).
|
|
141
|
+
const { id: positional, rest } = peelRolePositional(args);
|
|
142
|
+
// A value-taking write flag immediately followed by a `--`-flag means the operator forgot its
|
|
143
|
+
// value: `role qa --model --json` (or `--model --provider pi`, `--model --thinking high`)
|
|
144
|
+
// would otherwise take the next flag as the model id and fail later (or in
|
|
145
|
+
// rejectUnknownArgs) with a confusing message ("--json does not apply to a model write",
|
|
146
|
+
// "unknown argument: y") instead of naming the actual slip. Refuse up front with the flag's
|
|
147
|
+
// own needs-a-value wording — the same spirit as `config set`'s flag-looking-value guard.
|
|
148
|
+
// Only a `--` (double-dash) token reads as a flag: a single-dash token like `-1` is a
|
|
149
|
+
// negative-number value that must reach the setters' own bound checks, and the file flags'
|
|
150
|
+
// `-` stdin value is not in this set at all.
|
|
151
|
+
for (const flag of ["--provider", "--model", "--thinking", "--cap", "--min-run-interval", "--instances", "--quiet-hours"]) {
|
|
152
|
+
const at = rest.indexOf(flag);
|
|
153
|
+
const next = at >= 0 ? rest[at + 1] : undefined;
|
|
154
|
+
if (next?.startsWith("--"))
|
|
155
|
+
fail(`${flag} needs a value (the next token ${JSON.stringify(next)} is a flag, not a value)`);
|
|
156
|
+
}
|
|
157
|
+
rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC]);
|
|
158
|
+
refuseTeamWorkspace(root);
|
|
159
|
+
// The id is required: positional (`tumwater role <id>`) or --role <id> (the flag
|
|
160
|
+
// spelling every other role-targeting command shares). Both at once is a mistake, not
|
|
161
|
+
// a silent pick of one.
|
|
162
|
+
const flagId = flagValue(rest, "--role") ?? null;
|
|
163
|
+
const typedId = positional ?? flagId;
|
|
164
|
+
if (!typedId)
|
|
165
|
+
fail("tumwater role needs a role id");
|
|
166
|
+
const id = canonicalRoleId(typedId);
|
|
167
|
+
if (positional !== null && flagId !== null && flagId !== positional)
|
|
168
|
+
fail("give the role id once — as the positional or as --role <id>, not both");
|
|
169
|
+
const clear = rest.includes("--clear-notes");
|
|
170
|
+
const notesFile = flagValue(rest, "--notes-file");
|
|
171
|
+
const clearInst = rest.includes("--clear-instructions");
|
|
172
|
+
const instFile = flagValue(rest, "--instructions-file");
|
|
173
|
+
const clearQuiet = rest.includes("--clear-quiet-hours");
|
|
174
|
+
const clearCap = rest.includes("--clear-cap");
|
|
175
|
+
const clearInterval = rest.includes("--clear-min-run-interval");
|
|
176
|
+
const clearInstances = rest.includes("--clear-instances");
|
|
177
|
+
const clearProvider = rest.includes("--clear-provider");
|
|
178
|
+
const clearModel = rest.includes("--clear-model");
|
|
179
|
+
const clearThinking = rest.includes("--clear-thinking");
|
|
180
|
+
const notesWrite = clear || rest.includes("--notes-file");
|
|
181
|
+
const instructionsWrite = clearInst || rest.includes("--instructions-file");
|
|
182
|
+
const quietWrite = clearQuiet || rest.includes("--quiet-hours");
|
|
183
|
+
const capWrite = clearCap || rest.includes("--cap");
|
|
184
|
+
const intervalWrite = clearInterval || rest.includes("--min-run-interval");
|
|
185
|
+
const instancesWrite = clearInstances || rest.includes("--instances");
|
|
186
|
+
const providerWrite = clearProvider || rest.includes("--provider");
|
|
187
|
+
const modelWrite = clearModel || rest.includes("--model");
|
|
188
|
+
const thinkingWrite = clearThinking || rest.includes("--thinking");
|
|
189
|
+
const enable = rest.includes("--enable");
|
|
190
|
+
const disable = rest.includes("--disable");
|
|
191
|
+
const writes = [];
|
|
192
|
+
if (notesWrite)
|
|
193
|
+
writes.push("notes");
|
|
194
|
+
if (instructionsWrite)
|
|
195
|
+
writes.push("instructions");
|
|
196
|
+
if (quietWrite)
|
|
197
|
+
writes.push("quiet-hours");
|
|
198
|
+
if (instancesWrite)
|
|
199
|
+
writes.push("instances");
|
|
200
|
+
if (capWrite)
|
|
201
|
+
writes.push("cap");
|
|
202
|
+
if (intervalWrite)
|
|
203
|
+
writes.push("min-run-interval");
|
|
204
|
+
if (providerWrite)
|
|
205
|
+
writes.push("provider");
|
|
206
|
+
if (modelWrite)
|
|
207
|
+
writes.push("model");
|
|
208
|
+
if (thinkingWrite)
|
|
209
|
+
writes.push("thinking");
|
|
210
|
+
if (enable)
|
|
211
|
+
writes.push("enable");
|
|
212
|
+
if (disable)
|
|
213
|
+
writes.push("disable");
|
|
214
|
+
if (writes.length > 1)
|
|
215
|
+
fail(`${writes.join(" and ")} writes cannot share one invocation`);
|
|
216
|
+
if (notesWrite) {
|
|
217
|
+
if (clear && rest.includes("--notes-file"))
|
|
218
|
+
fail("give --clear-notes or --notes-file, not both");
|
|
219
|
+
if (rest.includes("--json"))
|
|
220
|
+
fail("--json does not apply to a notes write");
|
|
221
|
+
if (id === DIRECTOR_ROLE)
|
|
222
|
+
fail("the director has no notes");
|
|
223
|
+
requireKnownRole(root, id);
|
|
224
|
+
const text = clear ? "" : readPromptFileArg(notesFile || fail("--notes-file needs a path"));
|
|
225
|
+
try {
|
|
226
|
+
say(`${id}: ${saveRoleNote(roleNotesPath(root, id), text)}`);
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
fail(errorMessage(err));
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// The instructions-override write: the file-shaped twin of the notes flags, but the value
|
|
234
|
+
// lands in roles.<base>.instructions (run-prompt reads config.roles[base]) through the
|
|
235
|
+
// same setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig
|
|
236
|
+
// enforces the 4096-char cap and the write is atomic. The director is allowed here (its
|
|
237
|
+
// override reaches its own runs), unlike the notes branch's director refusal.
|
|
238
|
+
if (instructionsWrite) {
|
|
239
|
+
if (clearInst && rest.includes("--instructions-file"))
|
|
240
|
+
fail("give --clear-instructions or --instructions-file, not both");
|
|
241
|
+
if (rest.includes("--json"))
|
|
242
|
+
fail("--json does not apply to an instructions write");
|
|
243
|
+
requireKnownRole(root, id);
|
|
244
|
+
const base = baseRoleOf(id);
|
|
245
|
+
if (clearInst) {
|
|
246
|
+
const result = unsetConfigKey(root, `roles.${base}.instructions`);
|
|
247
|
+
if (!result.ok)
|
|
248
|
+
fail(result.error);
|
|
249
|
+
say(`${base}: instructions cleared`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const text = readPromptFileArg(instFile || fail("--instructions-file needs a path"));
|
|
253
|
+
const result = setConfigKey(root, `roles.${base}.instructions`, JSON.stringify(text));
|
|
254
|
+
if (!result.ok)
|
|
255
|
+
fail(result.error);
|
|
256
|
+
say(`${base}: instructions saved (${text.length} chars)`);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// The quiet-hours write: the third write kind, the terminal twin of `config set
|
|
261
|
+
// quietHoursPerRole.<base> <window>`. The window lands in quietHoursPerRole.<base> through
|
|
262
|
+
// the same setConfigKey/unsetConfigKey core config set/unset use, so checkQuietHours (with
|
|
263
|
+
// the typed key as its label) and validateConfig own validation and the write is atomic.
|
|
264
|
+
// The director is exempt — its quietHoursPerRole.director entry is inert (the gate and
|
|
265
|
+
// doctor both exempt it), like the notes branch's director refusal.
|
|
266
|
+
if (quietWrite) {
|
|
267
|
+
if (clearQuiet && rest.includes("--quiet-hours"))
|
|
268
|
+
fail("give --clear-quiet-hours or --quiet-hours, not both");
|
|
269
|
+
if (rest.includes("--json"))
|
|
270
|
+
fail("--json does not apply to a quiet-hours write");
|
|
271
|
+
requireKnownRole(root, id);
|
|
272
|
+
if (id === DIRECTOR_ROLE)
|
|
273
|
+
fail("the director has no quiet hours");
|
|
274
|
+
const base = baseRoleOf(id);
|
|
275
|
+
const window = flagValue(rest, "--quiet-hours") ?? "";
|
|
276
|
+
writeRoleConfigKey(root, base, clearQuiet, window, `quietHoursPerRole.${base}`, "quiet hours", (v) => JSON.stringify(v), (v) => v);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// The daily-cap write: the terminal twin of `config set maxDailyCostUsdPerRole.<base>
|
|
280
|
+
// <usd>`, the two remaining per-role holds' write the dashboard and the TUI already own.
|
|
281
|
+
// The value lands in maxDailyCostUsdPerRole.<base> through the same
|
|
282
|
+
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so checkDailyBudgetUsd
|
|
283
|
+
// (with the typed key as its label) and validateConfig own validation and the write is
|
|
284
|
+
// atomic. The director is exempt — it has no cap, like the notes and quiet-hours branches'
|
|
285
|
+
// director refusals.
|
|
286
|
+
if (capWrite) {
|
|
287
|
+
if (clearCap && rest.includes("--cap"))
|
|
288
|
+
fail("give --clear-cap or --cap, not both");
|
|
289
|
+
if (rest.includes("--json"))
|
|
290
|
+
fail("--json does not apply to a cap write");
|
|
291
|
+
requireKnownRole(root, id);
|
|
292
|
+
if (id === DIRECTOR_ROLE)
|
|
293
|
+
fail("the director has no daily cap");
|
|
294
|
+
const base = baseRoleOf(id);
|
|
295
|
+
const cap = flagValue(rest, "--cap") ?? "";
|
|
296
|
+
writeRoleConfigKey(root, base, clearCap, cap, `maxDailyCostUsdPerRole.${base}`, "daily cap", (v) => plainDecimalToJson(v) ?? v, (v) => v);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// The min-run-interval write: the terminal twin of `config set
|
|
300
|
+
// roles.<base>.minRunIntervalSeconds <seconds>`, the fourth per-role hold's write. The
|
|
301
|
+
// value lands in roles.<base>.minRunIntervalSeconds through the same
|
|
302
|
+
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
|
|
303
|
+
// roles.<id>.minRunIntervalSeconds DURATION_NON_NEGATIVE check owns validation and the
|
|
304
|
+
// write is atomic. Unlike the notes/quiet-hours/cap branches there is no director refusal:
|
|
305
|
+
// the director runs on the same clock.
|
|
306
|
+
if (intervalWrite) {
|
|
307
|
+
if (clearInterval && rest.includes("--min-run-interval"))
|
|
308
|
+
fail("give --clear-min-run-interval or --min-run-interval, not both");
|
|
309
|
+
if (rest.includes("--json"))
|
|
310
|
+
fail("--json does not apply to a min-run-interval write");
|
|
311
|
+
requireKnownRole(root, id);
|
|
312
|
+
const base = baseRoleOf(id);
|
|
313
|
+
const secs = flagValue(rest, "--min-run-interval") ?? "";
|
|
314
|
+
writeRoleConfigKey(root, base, clearInterval, secs, `roles.${base}.minRunIntervalSeconds`, "min run interval", (v) => plainDecimalToJson(v) ?? v, (v) => `${v}s`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// The instances write: the terminal twin of `config set roles.<base>.instances <n>`, the
|
|
318
|
+
// last per-role hold's write. The count lands in roles.<base>.instances through the same
|
|
319
|
+
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
|
|
320
|
+
// roles.<id>.instances INSTANCE_COUNT check owns the bound and the write is atomic. As
|
|
321
|
+
// with min run interval there is no director (or non-instance-role) refusal of its own:
|
|
322
|
+
// validateConfig's `!INSTANCE_ROLES.has(id)` branch answers for the director and every
|
|
323
|
+
// custom role, and an unset/empty value clears the key.
|
|
324
|
+
if (instancesWrite) {
|
|
325
|
+
if (clearInstances && rest.includes("--instances"))
|
|
326
|
+
fail("give --clear-instances or --instances, not both");
|
|
327
|
+
if (rest.includes("--json"))
|
|
328
|
+
fail("--json does not apply to an instances write");
|
|
329
|
+
const loaded = loadConfigSafe(root);
|
|
330
|
+
const cfg = loaded.config ?? defaultConfig();
|
|
331
|
+
if (!knownRoleIds(cfg).includes(id))
|
|
332
|
+
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
333
|
+
const base = baseRoleOf(id);
|
|
334
|
+
const n = flagValue(rest, "--instances") ?? "";
|
|
335
|
+
// An absent count already means 1 (instanceCount's default), so asking for 1 is a no-op —
|
|
336
|
+
// but only for a role that may split; any other role falls through to validateConfig's refusal.
|
|
337
|
+
// A tumwater.json that fails to load is no basis for "already": fall through to the failure.
|
|
338
|
+
if (loaded.config && !clearInstances && n.trim() === "1" && INSTANCE_ROLES.has(base) && cfg.roles[base]?.instances === undefined) {
|
|
339
|
+
say(`${base}: instances is already 1, so tumwater.json was not written`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
writeRoleConfigKey(root, base, clearInstances, n, `roles.${base}.instances`, "instances", (v) => v, (v) => v);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// The model-triple writes: the terminal twins of `config set roles.<base>.provider|model|thinking
|
|
346
|
+
// <value>` and `config unset` — the three fields configForRole resolves through seamOverrides.
|
|
347
|
+
// `tumwater role <id>` renders them as its Model: line. Each lands in roles.<base>.<field>.
|
|
348
|
+
// Through setConfigKey/unsetConfigKey, validateConfig's checkModelTripleField ("tier-name"
|
|
349
|
+
// shape) owns validation (model accepts a tier name; thinking must be a THINKING_LEVELS level)
|
|
350
|
+
// and each write is atomic. No director refusal: accepted exactly as --min-run-interval is.
|
|
351
|
+
if (providerWrite) {
|
|
352
|
+
if (clearProvider && rest.includes("--provider"))
|
|
353
|
+
fail("give --clear-provider or --provider, not both");
|
|
354
|
+
if (rest.includes("--json"))
|
|
355
|
+
fail("--json does not apply to a provider write");
|
|
356
|
+
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
357
|
+
if (!knownRoleIds(cfg).includes(id))
|
|
358
|
+
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
359
|
+
const base = baseRoleOf(id);
|
|
360
|
+
const provider = flagValue(rest, "--provider") ?? "";
|
|
361
|
+
writeRoleConfigKey(root, base, clearProvider, provider, `roles.${base}.provider`, "provider", (v) => JSON.stringify(v), (v) => v);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (modelWrite) {
|
|
365
|
+
if (clearModel && rest.includes("--model"))
|
|
366
|
+
fail("give --clear-model or --model, not both");
|
|
367
|
+
if (rest.includes("--json"))
|
|
368
|
+
fail("--json does not apply to a model write");
|
|
369
|
+
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
370
|
+
if (!knownRoleIds(cfg).includes(id))
|
|
371
|
+
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
372
|
+
const base = baseRoleOf(id);
|
|
373
|
+
const model = flagValue(rest, "--model") ?? "";
|
|
374
|
+
writeRoleConfigKey(root, base, clearModel, model, `roles.${base}.model`, "model", (v) => JSON.stringify(v), (v) => v);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (thinkingWrite) {
|
|
378
|
+
if (clearThinking && rest.includes("--thinking"))
|
|
379
|
+
fail("give --clear-thinking or --thinking, not both");
|
|
380
|
+
if (rest.includes("--json"))
|
|
381
|
+
fail("--json does not apply to a thinking write");
|
|
382
|
+
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
383
|
+
if (!knownRoleIds(cfg).includes(id))
|
|
384
|
+
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
385
|
+
const base = baseRoleOf(id);
|
|
386
|
+
const level = flagValue(rest, "--thinking") ?? "";
|
|
387
|
+
writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// The enable/disable write: the scoped write for the enabled state `tumwater role <id>`
|
|
391
|
+
// already renders, the terminal twin of `config set roles.<base>.enabled true|false`. The
|
|
392
|
+
// value lands in roles.<base>.enabled through the same setConfigKey core config set uses, so
|
|
393
|
+
// validateConfig's checkBoolean (the roles.<id>. prefix) owns the type and the write stays
|
|
394
|
+
// atomic and applies live. The director cannot be disabled: with it disabled runnerIds omits
|
|
395
|
+
// it and no role dequeues the operator's inbox, so that refusal is explicit (enabling stays
|
|
396
|
+
// allowed). knownRoleIds holds every catalog and custom role whether or not it is enabled, so
|
|
397
|
+
// a currently-disabled id resolves; an instance id writes its base role's key. A role already
|
|
398
|
+
// in the asked-for state is reported as such and the file is left alone.
|
|
399
|
+
if (enable || disable) {
|
|
400
|
+
if (rest.includes("--json"))
|
|
401
|
+
fail("--json does not apply to an enable/disable write");
|
|
402
|
+
requireKnownRole(root, id);
|
|
403
|
+
if (disable && id === DIRECTOR_ROLE)
|
|
404
|
+
fail("the director cannot be disabled");
|
|
405
|
+
const base = baseRoleOf(id);
|
|
406
|
+
const state = enable ? "enabled" : "disabled";
|
|
407
|
+
if (loadConfigSafe(root).config?.roles[base]?.enabled === enable) {
|
|
408
|
+
say(`${base}: already ${state}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const result = setConfigKey(root, `roles.${base}.enabled`, enable ? "true" : "false");
|
|
412
|
+
if (!result.ok)
|
|
413
|
+
fail(result.error);
|
|
414
|
+
say(`${base}: ${state}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// The unknown-role answer (exit 1, unknownRoleMessage's wording) lives in the
|
|
418
|
+
// collector; here the id just has to be non-null for the thunk's types.
|
|
419
|
+
sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
|
|
420
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** `tumwater start`'s exit-summary half: the machinery that turns a finished round or window
|
|
2
|
+
* into its summary — the `--once` one-line report and its `--json` document, and the `--for`
|
|
3
|
+
* window summary table and report. `src/cli/cli-start.ts` keeps `cmdStart` (the boot and the
|
|
4
|
+
* run loop), the window's deadline and scheduled-stop watchdogs, the `--gui` child plan, and
|
|
5
|
+
* the supervisor half; this module holds only the reporting that a `start`'s exit prints, so
|
|
6
|
+
* the largest command module is no longer also the home of every way a run's tally is shaped.
|
|
7
|
+
* `parseRunWindow` keeps `--for`'s shape and ceiling rules together with the window summary
|
|
8
|
+
* they feed. */
|
|
9
|
+
import { compareCodeUnits } from "../collections.js";
|
|
10
|
+
import { plural } from "../text/phrases.js";
|
|
11
|
+
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
12
|
+
import { renderSummary } from "../history/history-render.js";
|
|
13
|
+
import { failOverDurationCap, failRivalShapes, flagValue, parseDurationFlag, } from "./cli-args.js";
|
|
14
|
+
import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
|
|
15
|
+
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
16
|
+
import { stateSkipReason } from "../runner/runner-state.js";
|
|
17
|
+
import { isTeamPaused, pausedRoles } from "../team/team-state.js";
|
|
18
|
+
import { runnerPaused } from "../roles/runner-ids.js";
|
|
19
|
+
/** The window start a generation adopts: the supervisor's RUN_WINDOW_START_ENV mark when it is a
|
|
20
|
+
* positive integer no later than `now`, else `now` (absent, malformed, or in the future). */
|
|
21
|
+
export function inheritedWindowStart(raw, now) {
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
return raw !== undefined && Number.isInteger(n) && n > 0 && n <= now ? n : now;
|
|
24
|
+
}
|
|
25
|
+
/** The `run --for` exit summary: a window line, then the `history --summary` table for the runs
|
|
26
|
+
* that ended since the window opened — every run counted once by its outcome class. Only the
|
|
27
|
+
* final exit prints it; a mid-window self-redeploy hands off silently and the next generation
|
|
28
|
+
* inherits the window start, so the table spans every generation. */
|
|
29
|
+
export function windowSummary(label, summary, covered) {
|
|
30
|
+
const { total } = summary;
|
|
31
|
+
const lines = [
|
|
32
|
+
total.runs === 0
|
|
33
|
+
? `for ${label}: 0 runs — nothing ran`
|
|
34
|
+
: `for ${label}: ${plural(total.runs, "run")} — ${total.changes} changes, ${total.none} none, ${total.refused} refused, ${total.problems} problems`,
|
|
35
|
+
];
|
|
36
|
+
if (total.runs > 0)
|
|
37
|
+
lines.push(renderSummary(summary));
|
|
38
|
+
if (!covered)
|
|
39
|
+
lines.push(SPARSE_WINDOW_NOTE);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** Build the `run --for --json` window report from the same summary `windowSummary` renders. */
|
|
43
|
+
export function windowReport(label, summary, covered) {
|
|
44
|
+
return { window: label, covered, roles: summary.roles, total: summary.total };
|
|
45
|
+
}
|
|
46
|
+
/** Parse `run --for <duration>` (the windowed run): the parsed duration in milliseconds, or null
|
|
47
|
+
* when the flag is absent. The two body-level rules fail fast, before any repo gate or boot: the
|
|
48
|
+
* value must parse as a duration (parseDurationFlag's own wording, the same messages the
|
|
49
|
+
* dispatcher's durationFlagSpec("run --for") gate already ran), and `--for` and `--once` are rivals
|
|
50
|
+
* — a one-round run and a windowed run cannot both apply. The 90-day ceiling is `pause --for`'s
|
|
51
|
+
* (PAUSE_FOR_MAX_MS), the same cap every scheduled window in the harness honors. Exported for
|
|
52
|
+
* tests, exactly as onceSummary below is. */
|
|
53
|
+
export function parseRunWindow(args, once) {
|
|
54
|
+
const raw = flagValue(args, "--for");
|
|
55
|
+
if (raw === null)
|
|
56
|
+
return null;
|
|
57
|
+
const ms = parseDurationFlag("start --for", raw);
|
|
58
|
+
if (once)
|
|
59
|
+
failRivalShapes("--for", "--once", "a one-round run and a windowed run cannot both apply");
|
|
60
|
+
failOverDurationCap("--for", ms, PAUSE_FOR_MAX_MS);
|
|
61
|
+
return ms;
|
|
62
|
+
}
|
|
63
|
+
/** Results that mean the harness or the project's check failed and the round's work did not
|
|
64
|
+
* merge: a `--once` round with a role ending in one exits ONCE_FAILED_EXIT_CODE. `rejected` and
|
|
65
|
+
* `refused` are the review gate and the agent working as designed; `aborted`, `quiet_killed`, and
|
|
66
|
+
* `user_aborted` only reach a once round on a shutdown or operator abort, which exit
|
|
67
|
+
* their own way. */
|
|
68
|
+
export const ONCE_FAILURE_RESULTS = new Set([
|
|
69
|
+
"error",
|
|
70
|
+
"review_error",
|
|
71
|
+
"merge_conflict",
|
|
72
|
+
"merge_blocked",
|
|
73
|
+
"main_red",
|
|
74
|
+
]);
|
|
75
|
+
function runsThisRound(s, role, runsBefore, runsDone) {
|
|
76
|
+
return runsDone ? (runsDone.get(role) ?? 0) : s.runs - (runsBefore.get(role) ?? 0);
|
|
77
|
+
}
|
|
78
|
+
/** Every role that could have run in this once round — `roles` in order plus any role that
|
|
79
|
+
* appears only in `runsDone` (enabled mid-round after `runsBefore` was taken) — paired with
|
|
80
|
+
* its loaded role state and the number of runs it made this round. The same per-role setup
|
|
81
|
+
* onceFailures and onceSummary both repeat, folded here so the two summary passes cannot
|
|
82
|
+
* drift on which roles they visit or how they count a round's work. */
|
|
83
|
+
function onceRoundRoles(root, roles, runsBefore, runsDone) {
|
|
84
|
+
const extra = runsDone ? [...runsDone.keys()].filter((r) => !roles.includes(r)) : [];
|
|
85
|
+
return [...roles, ...extra].map((role) => {
|
|
86
|
+
const state = loadRunnerState(root, role);
|
|
87
|
+
return { role, state, ran: runsThisRound(state, role, runsBefore, runsDone) };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** The roles that ran this round and whose last result is in ONCE_FAILURE_RESULTS. */
|
|
91
|
+
export function onceFailures(root, roles, runsBefore, runsDone) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const { role, state, ran } of onceRoundRoles(root, roles, runsBefore, runsDone)) {
|
|
94
|
+
if (ran > 0 && state.lastResult && ONCE_FAILURE_RESULTS.has(state.lastResult)) {
|
|
95
|
+
out.push({ role, result: state.lastResult });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** The once round's one-line summary, read from the runners' persisted role state plus the
|
|
101
|
+
* orchestrator's own settle reasons — a cron job's log shows what the round did without parsing
|
|
102
|
+
* events. Roles whose run counter advanced bucket by their last completed result; the rest are
|
|
103
|
+
* skipped with the reason the orchestrator settled them for (`paused`, `resume pending`,
|
|
104
|
+
* `backoff`, `deferred`, `disabled`, `idle` — handed back on OrchestratorExit.settled, so a
|
|
105
|
+
* deferred role reads as set aside, not as "nothing was due"); a role the map lacks (defensively
|
|
106
|
+
* — the round only exits once every role is settled) falls back to deriving the reason from
|
|
107
|
+
* state: a pause marker when one is held, a pending resume when one is flagged, backoff when
|
|
108
|
+
* `backoffSeconds` is raised, otherwise idle — the same classification settleSkipped draws,
|
|
109
|
+
* and keyed on the same signal: backoffSeconds is the backoff indicator, while nextRunAt is
|
|
110
|
+
* shared with the scheduled clock a productive run also writes (with backoffSeconds 0), so a
|
|
111
|
+
* future nextRunAt alone reads as a not-yet-due role, not as backoff (BUGS.md 2026-09-25,
|
|
112
|
+
* the same conflation settleSkipped itself carried).
|
|
113
|
+
*
|
|
114
|
+
* `runsDone`, when the caller hands it back (the orchestrator's OnceRound snapshot deltas),
|
|
115
|
+
* is the run count's source of truth and also names roles the pre-round list never had —
|
|
116
|
+
* a role enabled mid-round joins the runners array after `runsBefore` was taken, so its
|
|
117
|
+
* whole persisted history must not be counted as this round's work. Without it the old
|
|
118
|
+
* `runsBefore` delta path runs, for callers (and tests) without a round object. */
|
|
119
|
+
export function onceReport(root, roles, runsBefore, settled, runsDone) {
|
|
120
|
+
const report = {
|
|
121
|
+
runs: 0,
|
|
122
|
+
outcomes: {},
|
|
123
|
+
skipped: [],
|
|
124
|
+
failures: onceFailures(root, roles, runsBefore, runsDone),
|
|
125
|
+
};
|
|
126
|
+
for (const { role, state, ran } of onceRoundRoles(root, roles, runsBefore, runsDone)) {
|
|
127
|
+
if (ran > 0) {
|
|
128
|
+
report.runs += ran;
|
|
129
|
+
const key = state.lastResult ?? "no_change";
|
|
130
|
+
report.outcomes[key] = (report.outcomes[key] ?? 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const reason = settled?.get(role) ??
|
|
134
|
+
(isTeamPaused(root) || runnerPaused(new Set(pausedRoles(root)), role)
|
|
135
|
+
? "paused"
|
|
136
|
+
: stateSkipReason(state));
|
|
137
|
+
report.skipped.push({ role, reason });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return report;
|
|
141
|
+
}
|
|
142
|
+
/** The once round's one-line summary — the composition of onceReport and renderOnceSummary,
|
|
143
|
+
* kept as a single function so its exported signature (and every caller and test) stays
|
|
144
|
+
* unchanged while the `--json` document renders the same facts structurally. */
|
|
145
|
+
export function onceSummary(root, roles, runsBefore, settled, runsDone) {
|
|
146
|
+
return renderOnceSummary(onceReport(root, roles, runsBefore, settled, runsDone));
|
|
147
|
+
}
|
|
148
|
+
/** Render a OnceReport back to the once round's one-line prose summary — the exact line a
|
|
149
|
+
* `tumwater start --once` (no `--json`) operator's log keeps. Results are counted in
|
|
150
|
+
* compareCodeUnits-sorted key order. The comparator makes that explicit: a comparator-less
|
|
151
|
+
* sort would order key/value pairs by their "key,count" string form, which happens to agree
|
|
152
|
+
* only while each key is unique. */
|
|
153
|
+
export function renderOnceSummary(report) {
|
|
154
|
+
const counts = Object.entries(report.outcomes)
|
|
155
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
156
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
157
|
+
.join(", ");
|
|
158
|
+
const skipNote = report.skipped.length === 0 ? "" : `, ${report.skipped.length} skipped (${report.skipped.map((s) => s.reason).join(", ")})`;
|
|
159
|
+
return `once: ${plural(report.runs, "run")} — ${counts || "nothing ran"}${skipNote}`;
|
|
160
|
+
}
|