tumwater 0.2.0 → 0.4.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 +55 -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 +6 -4
- 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 +404 -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 +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -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 +264 -0
- package/dist/src/backlog/backlog-release.js +134 -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-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -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 +65 -3
- 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 +181 -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 +183 -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 +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- 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 +16 -1
- package/dist/src/config/config-fallbacks.js +94 -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 +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- 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 +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- 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 +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- 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 +89 -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 +54 -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 +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- 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 +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- 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 +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -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 +28 -17
- 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 +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- 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 +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -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 +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -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 +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -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 +126 -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 +26 -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 +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -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 +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- 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 +180 -401
- 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 +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -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} +122 -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} +170 -83
- 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} +105 -57
- 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 +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -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} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- 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 +49 -17
- 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 +43 -28
- package/dist/src/status/status-roles.js +50 -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 +7 -8
- 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 +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- 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 +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -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 +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- 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 +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -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 +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -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 +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -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 +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- 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,128 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
/** The read-only query commands' bodies (status, diff, backlog, questions, role), split out of
|
|
1
|
+
/** The read-only query commands' bodies (status, diff, queue, backlog), split out of
|
|
3
2
|
* cli.ts's dispatcher: main() routes to a cmd* here exactly as it routes to operator-commands's
|
|
4
3
|
* cmdAbort or config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
|
|
5
4
|
* surfaces. Each command owns its argument gate, ready-repo gating decision, and output shape,
|
|
6
5
|
* with the wording and gating order moved verbatim from main()'s cases. */
|
|
7
|
-
import { fail, say, sayJson, sayJsonOrRender
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
6
|
+
import { fail, say, sayJson, sayJsonOrRender } from "./cli-output.js";
|
|
7
|
+
import { cancelQueuedMerge } from "../merge/merge-cancel.js";
|
|
8
|
+
import { moveQueuedMergeToTop } from "../merge/merge-top.js";
|
|
9
|
+
import { followView } from "./follow-view.js";
|
|
10
|
+
import { flagValue, parseGrepFlag, parseRoleListScope, rejectRepeatFlag, scanPositionArgs } from "./cli-args.js";
|
|
11
|
+
import { DIFF_PATH_FLAG, FOLLOW_FLAG, hasFollowFlag, JSON_FLAG, READY_FLAG, grepFlagSpec, rejectUnknownArgs, OVERLAPS_FLAG, ROLE_FLAG, STAT_FLAG } from "./cli-flag-specs.js";
|
|
12
|
+
import { refuseTeamWorkspace, requireReadyRepo } from "./cli-repo-gate.js";
|
|
13
|
+
import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
|
|
14
14
|
import { statusPayload } from "../ui/status-payload.js";
|
|
15
15
|
import { renderStatus } from "../ui/status-render.js";
|
|
16
16
|
import { backlogPayload } from "../backlog/backlog.js";
|
|
17
17
|
import { backlogHolds, backlogInProgress } from "../backlog/backlog-eligibility.js";
|
|
18
|
-
import { readyBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
|
|
19
|
+
import { loadQuarantine } from "../review/known-flakes.js";
|
|
20
|
+
import { quarantineRows, renderQuarantine } from "../review/quarantine-render.js";
|
|
21
|
+
import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, collectRoleChanges, normalizeDiffPath } from "../change/change-data.js";
|
|
22
|
+
import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
|
|
21
23
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
22
|
-
import { renderMergeQueue } from "../merge/merge-queue-render.js";
|
|
24
|
+
import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.js";
|
|
25
|
+
import { mergeChanges } from "../merge/merge-slot.js";
|
|
26
|
+
import { shortSha } from "../text/format.js";
|
|
27
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
23
28
|
import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
24
29
|
import { queueInFlight } from "../status/status-polls.js";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import { answerEditorBuffer, answerQuestion, answerQuestionFromText, decisionFromEditorBuffer, sayAnswered, sayQuestionList } from "./question-commands.js";
|
|
36
|
-
import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, questionListPayload, renderShownQuestion } from "../backlog/question-answer.js";
|
|
37
|
-
import { editInEditor } from "./cli-editor.js";
|
|
38
|
-
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
39
|
-
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
40
|
-
import { setConfigKey, unsetConfigKey } from "../config/config-write.js";
|
|
41
|
-
/** `questions answer`'s `--file <path>` flag: the decision comes from the file, lines kept. */
|
|
42
|
-
const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
|
|
43
|
-
/** `role`'s notebook-write flags: clear the note, or replace it from a file (`-` for stdin). */
|
|
44
|
-
const CLEAR_NOTES_SPEC = { names: ["--clear-notes"] };
|
|
45
|
-
const NOTES_FILE_SPEC = { names: ["--notes-file"], value: true, valueName: "<path>" };
|
|
46
|
-
/** `role`'s instructions-override flags: clear it, or replace it from a file (`-` for stdin). */
|
|
47
|
-
const CLEAR_INSTRUCTIONS_SPEC = { names: ["--clear-instructions"] };
|
|
48
|
-
const INSTRUCTIONS_FILE_SPEC = { names: ["--instructions-file"], value: true, valueName: "<path>" };
|
|
49
|
-
/** `role`'s quiet-hours write flags: clear the window, or set it to a daily local-time window. */
|
|
50
|
-
const CLEAR_QUIET_HOURS_SPEC = { names: ["--clear-quiet-hours"] };
|
|
51
|
-
const QUIET_HOURS_SPEC = { names: ["--quiet-hours"], value: true, valueName: "<window>" };
|
|
52
|
-
/** `role`'s daily-cap write flags: clear the cap, or set it to a USD amount (0 disables). */
|
|
53
|
-
const CLEAR_CAP_SPEC = { names: ["--clear-cap"] };
|
|
54
|
-
const CAP_SPEC = { names: ["--cap"], value: true, valueName: "<usd>" };
|
|
55
|
-
/** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
|
|
56
|
-
* back-to-back). */
|
|
57
|
-
const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
|
|
58
|
-
const MIN_RUN_INTERVAL_SPEC = { names: ["--min-run-interval"], value: true, valueName: "<seconds>" };
|
|
59
|
-
/** `role`'s instances write flags: clear the count, or set it to a parallel-runner count
|
|
60
|
-
* (an integer from 1 to 8; only feature and bugfix split). */
|
|
61
|
-
const CLEAR_INSTANCES_SPEC = { names: ["--clear-instances"] };
|
|
62
|
-
const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" };
|
|
63
|
-
/** `role`'s enabled-state write flags: enable or disable the role, written to
|
|
64
|
-
* roles.<base>.enabled. */
|
|
65
|
-
const ENABLE_SPEC = { names: ["--enable"] };
|
|
66
|
-
const DISABLE_SPEC = { names: ["--disable"] };
|
|
67
|
-
/** `role`'s model-triple write flags: clear the field, or set it, written to
|
|
68
|
-
* roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
|
|
69
|
-
* pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
|
|
70
|
-
const CLEAR_PROVIDER_SPEC = { names: ["--clear-provider"] };
|
|
71
|
-
const PROVIDER_SPEC = { names: ["--provider"], value: true, valueName: "<id>" };
|
|
72
|
-
const CLEAR_MODEL_SPEC = { names: ["--clear-model"] };
|
|
73
|
-
const MODEL_SPEC = { names: ["--model"], value: true, valueName: "<id>" };
|
|
74
|
-
const CLEAR_THINKING_SPEC = { names: ["--clear-thinking"] };
|
|
75
|
-
const THINKING_SPEC = { names: ["--thinking"], value: true, valueName: "<level>" };
|
|
76
|
-
/** Peel `tumwater role <id>`'s positional id off the argument list: the first token that is
|
|
77
|
-
* neither a flag nor the value of a --role, --notes-file, --instructions-file,
|
|
78
|
-
* --quiet-hours, --cap, --min-run-interval, --instances, --provider, --model or
|
|
79
|
-
* --thinking flag. Returns the id (null when absent) and the remaining arguments — flags
|
|
80
|
-
* only, ready for rejectUnknownArgs and sayJsonOrRender. The id may also arrive as
|
|
81
|
-
* --role <id>, the flag spelling every other role-targeting command shares; the
|
|
82
|
-
* collector's caller rejects both spellings at once rather than silently picking one.
|
|
83
|
-
* Lives here because it exists only for this one
|
|
84
|
-
* command's mixed positional/flag vocabulary. */
|
|
85
|
-
function peelRolePositional(args) {
|
|
86
|
-
const valueIdx = new Set();
|
|
87
|
-
for (const flag of ["--role", "--notes-file", "--instructions-file", "--quiet-hours", "--cap", "--min-run-interval", "--instances", "--provider", "--model", "--thinking"]) {
|
|
88
|
-
const at = args.indexOf(flag);
|
|
89
|
-
if (at >= 0)
|
|
90
|
-
valueIdx.add(at + 1);
|
|
91
|
-
}
|
|
92
|
-
let id = null;
|
|
93
|
-
const rest = [];
|
|
94
|
-
for (let i = 0; i < args.length; i++) {
|
|
95
|
-
const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
|
|
96
|
-
// A non-flag token that is not a value-taking flag's value is the positional,
|
|
97
|
-
// taken once; everything else — flags and their values alike — rides in rest for
|
|
98
|
-
// rejectUnknownArgs and flagValue to claim.
|
|
99
|
-
if (!arg.startsWith("-") && id === null && !valueIdx.has(i)) {
|
|
100
|
-
id = arg;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
rest.push(arg);
|
|
104
|
-
}
|
|
105
|
-
return { id, rest };
|
|
106
|
-
}
|
|
107
|
-
/** `tumwater status`: the team snapshot as rendered text or as the JSON document GET
|
|
108
|
-
* /api/status serves. */
|
|
109
|
-
export async function cmdStatus(root, args) {
|
|
110
|
-
rejectUnknownArgs("status", args, [JSON_FLAG]);
|
|
30
|
+
import { roleInScope } from "../roles/runner-ids.js";
|
|
31
|
+
/** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON
|
|
32
|
+
* document GET /api/status serves. `-f/--follow` repaints the table every couple of seconds
|
|
33
|
+
* until Ctrl+C or the process that started it exits, so a backgrounded follow does not
|
|
34
|
+
* outlive its parent. */
|
|
35
|
+
export async function cmdStatus(root, args, seams = {}) {
|
|
36
|
+
rejectUnknownArgs("status", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
|
|
37
|
+
const follow = hasFollowFlag(args);
|
|
38
|
+
if (follow && args.includes("--json"))
|
|
39
|
+
fail("status --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
111
40
|
await requireReadyRepo(root);
|
|
41
|
+
const scope = parseRoleListScope(root, args);
|
|
42
|
+
if (follow) {
|
|
43
|
+
await followStatus(root, scope, seams);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
112
46
|
if (args.includes("--json")) {
|
|
113
47
|
// Machine-readable team state — the document GET /api/status serves minus the
|
|
114
48
|
// serving process's own `serverBuildSha`, printed with no server. A query, not a
|
|
115
49
|
// health verdict: exit 0 on any successful read and let scripts interpret fields
|
|
116
50
|
// themselves ("running": false is data, not failure).
|
|
117
|
-
sayJson(statusPayload(root));
|
|
51
|
+
sayJson(statusPayload(root, Date.now(), scope));
|
|
118
52
|
}
|
|
119
53
|
else {
|
|
120
|
-
|
|
54
|
+
const snap = snapshot(root);
|
|
55
|
+
say(renderStatus(root, scope ? scopeSnapshotToRoles(snap, scope) : snap, process.stdout.isTTY ? process.stdout.columns : undefined, scope !== null));
|
|
121
56
|
}
|
|
122
57
|
}
|
|
123
|
-
/** `tumwater
|
|
124
|
-
|
|
125
|
-
|
|
58
|
+
/** `tumwater status -f`'s live loop: the team snapshot repainted until the parent exits. */
|
|
59
|
+
async function followStatus(root, scope, seams) {
|
|
60
|
+
const tty = seams.tty ?? process.stdout.isTTY;
|
|
61
|
+
await followView(() => renderStatus(root, scope ? scopeSnapshotToRoles(snapshot(root), scope) : snapshot(root), tty ? process.stdout.columns : undefined, scope !== null), seams);
|
|
62
|
+
}
|
|
63
|
+
/** `tumwater diff`: one line per role holding pending work, the files two or more roles touch
|
|
64
|
+
* with `--overlaps` (`--role` keeps those a listed role holds, `--path` those at or under it),
|
|
65
|
+
* or the full per-role view of each role listed with `--role <id>[,<id>...]`. */
|
|
66
|
+
export async function cmdDiff(root, args, seams = {}) {
|
|
126
67
|
// The repo half of requireReadyRepo still gates: a directory tumwater cannot read yet
|
|
127
68
|
// (no git, not a repository, no tumwater.json, no commits) has no team to ask about,
|
|
128
69
|
// and the change view's own degradation would misreport that as "main branch <name>
|
|
@@ -130,529 +71,281 @@ export async function cmdDiff(root, args) {
|
|
|
130
71
|
// command. Past the gate an absent worktree still degrades to a `no worktree for
|
|
131
72
|
// <role>` line (exit 0), so the command answers in any initialized directory —
|
|
132
73
|
// report's rationale.
|
|
133
|
-
rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG]);
|
|
74
|
+
rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG, OVERLAPS_FLAG, DIFF_PATH_FLAG, FOLLOW_FLAG]);
|
|
134
75
|
await requireReadyRepo(root);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
76
|
+
rejectRepeatFlag(args.filter((a) => a === "--path").length > 1, "--path");
|
|
77
|
+
const follow = hasFollowFlag(args);
|
|
78
|
+
if (follow && args.includes("--json"))
|
|
79
|
+
fail("diff --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
80
|
+
if (args.includes("--overlaps")) {
|
|
140
81
|
if (args.includes("--stat"))
|
|
141
|
-
fail("diff --
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// the render's short sha and age — and the render is the collector's view with one clock
|
|
160
|
-
// read, so `tumwater queue` and `tumwater status --json`'s mergeQueue cannot drift.
|
|
161
|
-
// The in-flight marker rides along only while an orchestrator runs and the marker names a
|
|
162
|
-
// queued entry (queueInFlight) — status --json's mergeQueue.inFlight, so the two agree.
|
|
163
|
-
const view = readMergeQueue(root);
|
|
164
|
-
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
165
|
-
sayJsonOrRender(args, inFlight ? { ...view, inFlight } : view, () => renderMergeQueue(view, Date.now(), inFlight));
|
|
166
|
-
}
|
|
167
|
-
/** `tumwater backlog`: the planned/open entries as Markdown or as the GUI's JSON payload. */
|
|
168
|
-
export async function cmdBacklog(root, args) {
|
|
169
|
-
// No requireReadyRepo gate: the entry readers degrade to [] on a missing file, so the
|
|
170
|
-
// command prints three empty sections in any directory (report's rationale, not config's).
|
|
171
|
-
rejectUnknownArgs("backlog", args, [JSON_FLAG, READY_FLAG]);
|
|
172
|
-
// Machine-readable backlog — the three entry arrays the Markdown view renders and the
|
|
173
|
-
// GUI's /api/backlog serves (status --json's "print the endpoint's payload" pattern),
|
|
174
|
-
// plus `holds.plans`/`holds.bugs` parallel to the entry arrays so a skipped entry's
|
|
175
|
-
// reason is data, not inference: a pretty-printed JSON document in every exit-0 case,
|
|
176
|
-
// never prose. The payload is a thunk, so whichever branch runs reads the three entry
|
|
177
|
-
// files through their stat caches once — the Markdown renderer consumes the same arrays
|
|
178
|
-
// the JSON document prints.
|
|
179
|
-
// --ready filters both branches through readyBacklog; the Markdown branch adds a count of
|
|
180
|
-
// the held entries it hid.
|
|
181
|
-
const ready = args.includes("--ready");
|
|
182
|
-
let hidden = 0;
|
|
183
|
-
sayJsonOrRender(args, () => {
|
|
184
|
-
const view = { ...backlogPayload(root), holds: backlogHolds(root), inProgress: backlogInProgress(root) };
|
|
185
|
-
if (!ready)
|
|
186
|
-
return view;
|
|
187
|
-
const kept = readyBacklog(view);
|
|
188
|
-
hidden = view.plans.length + view.bugs.length - kept.plans.length - kept.bugs.length;
|
|
189
|
-
return kept;
|
|
190
|
-
}, (v) => renderBacklogMarkdown(v) + (hidden > 0
|
|
191
|
-
? `\n\n_(${hidden} held entries hidden; run \`tumwater backlog\` to see them)_` : ""));
|
|
192
|
-
}
|
|
193
|
-
/** `tumwater questions`: list open questions, or answer one with `answer <n> <decision>`. */
|
|
194
|
-
export async function cmdQuestions(root, args) {
|
|
195
|
-
// No requireReadyRepo gate, like backlog: the reader degrades to an empty list on a
|
|
196
|
-
// missing QUESTIONS.md, so the command inspects any directory instead of refusing.
|
|
197
|
-
// The answer form's decision is free-form prose, so peelPositionals cannot route the
|
|
198
|
-
// tokens: a decision word may begin with a single dash (`questions answer 1 "-50% spend
|
|
199
|
-
// cap"`), and peelPositionals would hand it to the flag gate as an unknown flag — the
|
|
200
|
-
// same masking parsePromptArgs avoids for prompt text by owning only `--`-prefixed
|
|
201
|
-
// tokens. So the questions parse scans args itself: a `--json` token before the
|
|
202
|
-
// question's number positional is the one flag (repeats refused, equals form named by
|
|
203
|
-
// rejectEqualsForm), any other `--`-prefixed token before it is refused as unknown, and
|
|
204
|
-
// every remaining token in order is prose — subcommand, position number, then the
|
|
205
|
-
// decision words. Past the number positional, tokens are decision words even when one
|
|
206
|
-
// is spelled `--json`: unquoted decision prose (`questions answer 1 keep --json output`)
|
|
207
|
-
// reaches the command as separate argv tokens, and a flag scan that matched `--json`
|
|
208
|
-
// anywhere silently ate the token out of the recorded decision and flipped the command
|
|
209
|
-
// into JSON mode. Unknown `--`-prefixed tokens stay refused even there, so a misspelled
|
|
210
|
-
// flag is still an error rather than silent decision text.
|
|
211
|
-
const words = [];
|
|
212
|
-
let jsonFlag = false;
|
|
213
|
-
let numbered = false;
|
|
214
|
-
let file;
|
|
215
|
-
let answeredFlag = false;
|
|
216
|
-
let limit;
|
|
217
|
-
let grep;
|
|
218
|
-
let show;
|
|
219
|
-
for (let i = 0; i < args.length; i++) {
|
|
220
|
-
const arg = args[i];
|
|
221
|
-
if (!numbered && arg === "--show") {
|
|
222
|
-
rejectRepeatFlag(show !== undefined, "--show");
|
|
223
|
-
show = parsePositionFlag("--show", args[++i], "--show 2");
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (!numbered && arg === "--answered") {
|
|
227
|
-
rejectRepeatFlag(answeredFlag, "--answered");
|
|
228
|
-
answeredFlag = true;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (!numbered && arg === "-n") {
|
|
232
|
-
rejectRepeatFlag(limit !== undefined, "-n");
|
|
233
|
-
limit = parseCountFlag("-n", args[++i]);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (!numbered && arg === "--grep") {
|
|
237
|
-
rejectRepeatFlag(grep !== undefined, "--grep");
|
|
238
|
-
const text = args[++i];
|
|
239
|
-
if (text === undefined || text === "")
|
|
240
|
-
fail("--grep needs text (usage: tumwater questions --answered --grep <text>)");
|
|
241
|
-
grep = text;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
if (arg === "--file") {
|
|
245
|
-
rejectRepeatFlag(file !== undefined, "--file");
|
|
246
|
-
file = promptFileValue(args[++i]);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (!numbered && arg === "--json") {
|
|
250
|
-
rejectRepeatFlag(jsonFlag, "--json");
|
|
251
|
-
jsonFlag = true;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (arg.startsWith("--") && arg !== "--json") {
|
|
255
|
-
rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
|
|
256
|
-
fail(`unknown argument: ${arg} (valid flags for tumwater questions: --json, --file, --answered, --show, --grep)`);
|
|
257
|
-
}
|
|
258
|
-
if (!numbered) {
|
|
259
|
-
// The position is a plain-decimal positive integer (parsePositiveInt's rule, the same
|
|
260
|
-
// one --cancel's position honors): bare Number() admitted hex ("0x2"), exponent
|
|
261
|
-
// ("1e2"), and whitespace-padded spellings as a valid question number, answering a
|
|
262
|
-
// question the operator never named.
|
|
263
|
-
if (parsePositiveInt(arg) !== null)
|
|
264
|
-
numbered = true;
|
|
265
|
-
}
|
|
266
|
-
words.push(arg);
|
|
267
|
-
}
|
|
268
|
-
if (limit !== undefined && !answeredFlag)
|
|
269
|
-
fail("-n needs --answered (it limits the answered questions listed)");
|
|
270
|
-
if (grep !== undefined && !answeredFlag)
|
|
271
|
-
fail("--grep needs --answered (it searches the answered questions)");
|
|
272
|
-
if (show !== undefined) {
|
|
273
|
-
if (grep !== undefined)
|
|
274
|
-
fail("--grep does not apply to --show");
|
|
275
|
-
if (words.length > 0)
|
|
276
|
-
fail("--show prints one open question; it takes no subcommand");
|
|
277
|
-
if (file !== undefined)
|
|
278
|
-
fail("--file only applies to questions answer <n>");
|
|
279
|
-
if (answeredFlag) {
|
|
280
|
-
if (limit !== undefined)
|
|
281
|
-
fail("-n does not apply to --show");
|
|
282
|
-
const a = answeredQuestionAt(root, show);
|
|
283
|
-
if (a === null) {
|
|
284
|
-
const count = answeredQuestionCount(root);
|
|
285
|
-
const range = count > 0 ? ` — valid: 1-${count}` : "";
|
|
286
|
-
fail(`no answered question at position ${show} (${count} in QUESTIONS.md)${range}`);
|
|
287
|
-
}
|
|
288
|
-
if (jsonFlag)
|
|
289
|
-
sayJson(a);
|
|
290
|
-
else
|
|
291
|
-
say(renderShownQuestion(a));
|
|
82
|
+
fail("diff --overlaps prints paths, not patches; drop --stat");
|
|
83
|
+
// Overlaps are computed across every role; --role only filters which entries print.
|
|
84
|
+
const scope = parseRoleListScope(root, args);
|
|
85
|
+
const path = diffPathArg(args);
|
|
86
|
+
// The read and its zero-arg render are factored into one thunk so the follow's frame
|
|
87
|
+
// and the snapshot print exactly the same text from the same reader.
|
|
88
|
+
const readOverlaps = async () => {
|
|
89
|
+
let view = await collectChangeOverlaps(root);
|
|
90
|
+
if (scope === null && path === undefined)
|
|
91
|
+
return { view, render: () => renderChangeOverlaps(view) };
|
|
92
|
+
if (scope !== null)
|
|
93
|
+
view = scopeChangeOverlaps(view, scope);
|
|
94
|
+
if (path !== undefined)
|
|
95
|
+
view = scopeChangeOverlapsToPath(view, path);
|
|
96
|
+
return { view, render: () => renderChangeOverlaps(view, scope ?? undefined, path) };
|
|
97
|
+
};
|
|
98
|
+
if (follow) {
|
|
99
|
+
await followView(async () => (await readOverlaps()).render(), seams);
|
|
292
100
|
return;
|
|
293
101
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
const count = questionListPayload(root).questions.length;
|
|
297
|
-
const range = count > 0 ? ` — valid: 1-${count}` : "";
|
|
298
|
-
fail(`no open question at position ${show} (${count} in QUESTIONS.md)${range}`);
|
|
299
|
-
}
|
|
300
|
-
if (jsonFlag)
|
|
301
|
-
sayJson(q);
|
|
302
|
-
else
|
|
303
|
-
say(renderShownQuestion(q));
|
|
102
|
+
const { view, render } = await readOverlaps();
|
|
103
|
+
sayJsonOrRender(args, view, () => render());
|
|
304
104
|
return;
|
|
305
105
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
106
|
+
// Absent --role is the team-wide form: one line per role holding pending work
|
|
107
|
+
// (parseRoleListScope returns null only for an absent flag — an empty or unknown piece
|
|
108
|
+
// already failed above). One named role keeps the single per-role view; a list prints each
|
|
109
|
+
// listed role's view in order.
|
|
110
|
+
const roles = parseRoleListScope(root, args);
|
|
111
|
+
const path = roles === null ? undefined : diffPathArg(args);
|
|
112
|
+
// The roster/role reads below are factored into a thunk (view + zero-arg render) so the
|
|
113
|
+
// follow's frame and the snapshot share one reader and print byte-identical text.
|
|
114
|
+
const readChange = async () => {
|
|
115
|
+
if (roles === null) {
|
|
116
|
+
if (args.includes("--stat"))
|
|
117
|
+
fail("diff --stat needs --role <id> (the team roster prints no patch)");
|
|
118
|
+
if (args.includes("--path"))
|
|
119
|
+
fail("diff --path needs --role <id> (the team roster prints no patch)");
|
|
120
|
+
const view = await collectTeamChanges(root);
|
|
121
|
+
return { view, render: () => renderTeamChange(view) };
|
|
122
|
+
}
|
|
123
|
+
if (roles.length === 1) {
|
|
124
|
+
const change = await collectRoleChange(root, roles[0], { stat: args.includes("--stat"), path });
|
|
125
|
+
return { view: change, render: () => renderRoleChange(change) };
|
|
126
|
+
}
|
|
127
|
+
const view = await collectRoleChanges(root, roles, { stat: args.includes("--stat"), path });
|
|
128
|
+
return { view, render: () => renderRoleChanges(view) };
|
|
129
|
+
};
|
|
130
|
+
if (follow) {
|
|
131
|
+
await followView(async () => (await readChange()).render(), seams);
|
|
315
132
|
return;
|
|
316
133
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
134
|
+
const { view, render } = await readChange();
|
|
135
|
+
sayJsonOrRender(args, view, () => render());
|
|
136
|
+
}
|
|
137
|
+
/** The normalised `--path` value of `diff --role` (undefined when absent): leading `./` pieces
|
|
138
|
+
* and trailing `/` characters stripped, failing on an empty, absolute or `..`-bearing result. */
|
|
139
|
+
function diffPathArg(args) {
|
|
140
|
+
const raw = flagValue(args, "--path");
|
|
141
|
+
if (raw === null || raw === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const norm = normalizeDiffPath(raw);
|
|
144
|
+
if (norm === null)
|
|
145
|
+
fail("diff --path takes a repo-relative path such as src/cli.ts (as diff --overlaps prints it)");
|
|
146
|
+
return norm;
|
|
147
|
+
}
|
|
148
|
+
/** `tumwater queue [-f/--follow]`: the durable merge queue in execution order, as text or as
|
|
149
|
+
* the `{entries, unreadable}` document. `-f/--follow` repaints the queue every couple of
|
|
150
|
+
* seconds until Ctrl+C or the process that started it exits, so a backgrounded follow does
|
|
151
|
+
* not outlive its parent (the same rule `status -f` uses). */
|
|
152
|
+
export async function cmdQueue(root, args, seams = {}) {
|
|
153
|
+
// `tumwater queue --cancel <n>` is the read-only command's one destructive mode: route it
|
|
154
|
+
// to its own branch before the read gate, so --cancel never leaks into the view parsing.
|
|
155
|
+
if (args.includes("--cancel")) {
|
|
156
|
+
await cmdQueueCancel(root, args);
|
|
321
157
|
return;
|
|
322
158
|
}
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
// The offending token carries the same ` (got "...")` suffix every sibling number parser
|
|
326
|
-
// adds (parsePositiveInt's flag callers, --cancel); the bare `answer` form names none.
|
|
327
|
-
const raw = words.length > 1 ? words[1] : undefined;
|
|
328
|
-
const n = raw === undefined ? null : parsePositiveInt(raw);
|
|
329
|
-
if (n === null)
|
|
330
|
-
fail(`questions answer needs a positive question number: questions answer <n> "<decision>"${raw === undefined ? "" : gotSuffix(raw)}`);
|
|
331
|
-
if (file !== undefined) {
|
|
332
|
-
if (words.length > 2)
|
|
333
|
-
fail(`unexpected argument ${JSON.stringify(words[2])} — with --file the decision comes from the file`);
|
|
334
|
-
const answered = answerQuestionFromText(root, n, readPromptFileArg(file));
|
|
335
|
-
await commitAndReportAnswer(root, n, jsonFlag, answered.title, answered.decision);
|
|
159
|
+
if (args.includes("--top")) {
|
|
160
|
+
await cmdQueueTop(root, args);
|
|
336
161
|
return;
|
|
337
162
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
163
|
+
// The repo half of requireReadyRepo gates: like every repo-bound query, a directory
|
|
164
|
+
// tumwater cannot read yet has no queue to show. Past the gate an absent queue dir reads
|
|
165
|
+
// as an empty queue (a `merge queue empty` line, exit 0), so no merge ever queued is
|
|
166
|
+
// data, not an error.
|
|
167
|
+
rejectUnknownArgs("queue", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
|
|
168
|
+
await requireReadyRepo(root);
|
|
169
|
+
const roles = parseRoleListScope(root, args);
|
|
170
|
+
const follow = hasFollowFlag(args);
|
|
171
|
+
if (follow && args.includes("--json"))
|
|
172
|
+
fail("queue --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
173
|
+
// Scoped read shared by the follow and the one-shot branches, so the render's age and the
|
|
174
|
+
// in-flight marker share one clock read and each frame re-reads the queue. --role keeps
|
|
175
|
+
// the listed roles' entries (a base id taking its instances too) under their whole-queue
|
|
176
|
+
// positions; the in-flight marker is judged against the kept entries only, and
|
|
177
|
+
// `unreadable` stays whole because an unreadable file has no role to attribute it to.
|
|
178
|
+
const readQueue = () => {
|
|
179
|
+
const whole = readMergeQueue(root);
|
|
180
|
+
const scoped = roles ? scopeMergeQueue(whole, roles, (r) => roleInScope(r, roles)) : undefined;
|
|
181
|
+
const view = scoped ? scoped.view : whole;
|
|
182
|
+
return { view, inFlight: queueInFlight(root, view.entries, orchestratorAlive(root)), scope: scoped?.scope };
|
|
183
|
+
};
|
|
184
|
+
if (follow) {
|
|
185
|
+
await followView(() => {
|
|
186
|
+
const { view, inFlight, scope } = readQueue();
|
|
187
|
+
return renderMergeQueue(view, Date.now(), inFlight, scope);
|
|
188
|
+
}, seams);
|
|
351
189
|
return;
|
|
352
190
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// answerQuestion collapses the free-form decision to one line before writing it; report
|
|
356
|
-
// that written text in the --json payload so it matches the file, not the caller's spacing.
|
|
357
|
-
const { title, decision: writtenDecision } = answerQuestion(root, n, decision);
|
|
358
|
-
await commitAndReportAnswer(root, n, jsonFlag, title, writtenDecision);
|
|
359
|
-
}
|
|
360
|
-
/** Commit the answered question, report it in the requested output mode, and wake the
|
|
361
|
-
* asking role in prose mode — the tail every `questions answer` branch ends with. */
|
|
362
|
-
async function commitAndReportAnswer(root, n, jsonFlag, title, decision) {
|
|
363
|
-
warnIfUncommitted(await commitOperatorBacklogEdit(root, "QUESTIONS.md", stampedSubject("operator", `Answer question: ${title}`)), "QUESTIONS.md"); // vocabulary-ok: commit prefix stays
|
|
364
|
-
sayAnswered(n, title, jsonFlag, decision);
|
|
365
|
-
if (!jsonFlag)
|
|
366
|
-
sayAskerWake(root, title);
|
|
191
|
+
const { view, inFlight, scope } = readQueue();
|
|
192
|
+
sayJsonOrRender(args, inFlight ? { ...view, inFlight } : view, () => renderMergeQueue(view, Date.now(), inFlight, scope));
|
|
367
193
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
*
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
194
|
+
/** The `tumwater queue --cancel <n>` branch (PLANS.md): remove the Nth queued merge `tumwater
|
|
195
|
+
* queue` lists and delete its role's merge pin, so next-run leftover recovery cannot silently
|
|
196
|
+
* re-enqueue the change. Parsed as cancelAndAnnounce parses `--cancel` (backlog-cli.ts):
|
|
197
|
+
* `--cancel <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h
|
|
198
|
+
* tolerated, anything else refused. --role and -f/--follow are refused by name — a live view
|
|
199
|
+
* repaints a read, and position numbers come from the whole queue, whose numbering is
|
|
200
|
+
* `tumwater queue`'s. An entry a live merge marker names for this sha is refused (wait for it
|
|
201
|
+
* to land, then revert); an out-of-range position fails naming the current depth. */
|
|
202
|
+
async function cmdQueueCancel(root, args) {
|
|
203
|
+
const { json, position, rest } = scanPositionArgs(args, "--cancel", "--cancel 2");
|
|
204
|
+
// The vocabulary a cancel invocation accepts is exactly --cancel and --json, both consumed
|
|
205
|
+
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
206
|
+
if (rest.includes("--role"))
|
|
207
|
+
fail("queue --cancel cancels by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
|
|
208
|
+
if (rest.includes("--follow") || rest.includes("-f"))
|
|
209
|
+
fail("queue --cancel removes one entry; -f/--follow repaints a live view — drop it");
|
|
210
|
+
rejectUnknownArgs("queue", rest, []);
|
|
211
|
+
if (position === null)
|
|
212
|
+
fail("--cancel needs a position (usage: tumwater queue --cancel <n>)");
|
|
213
|
+
await requireReadyRepo(root);
|
|
214
|
+
// Refuse the entry a live merge marker names before touching it: it is mid-flight (being
|
|
215
|
+
// reviewed or merged), so cancelling it would race the slot that owns it.
|
|
216
|
+
const view = readMergeQueue(root);
|
|
217
|
+
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
218
|
+
const target = view.entries[position - 1];
|
|
219
|
+
if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
|
|
220
|
+
fail(`queue entry ${position} is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(target.sha)}\``);
|
|
221
|
+
}
|
|
222
|
+
const outcome = await cancelQueuedMerge(root, position);
|
|
223
|
+
if (outcome.status === "out-of-range") {
|
|
224
|
+
fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
|
|
225
|
+
}
|
|
226
|
+
if (json) {
|
|
227
|
+
sayJson({ cancelled: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
396
228
|
}
|
|
397
229
|
else {
|
|
398
|
-
|
|
399
|
-
if (!result.ok)
|
|
400
|
-
fail(result.error);
|
|
401
|
-
say(`${base}: ${noun} set to ${display(raw)}`);
|
|
230
|
+
say(`cancelled queue entry ${position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
402
231
|
}
|
|
403
232
|
}
|
|
404
|
-
/** `tumwater
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
fail("
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const providerWrite = clearProvider || rest.includes("--provider");
|
|
439
|
-
const modelWrite = clearModel || rest.includes("--model");
|
|
440
|
-
const thinkingWrite = clearThinking || rest.includes("--thinking");
|
|
441
|
-
const enable = rest.includes("--enable");
|
|
442
|
-
const disable = rest.includes("--disable");
|
|
443
|
-
const writes = [];
|
|
444
|
-
if (notesWrite)
|
|
445
|
-
writes.push("notes");
|
|
446
|
-
if (instructionsWrite)
|
|
447
|
-
writes.push("instructions");
|
|
448
|
-
if (quietWrite)
|
|
449
|
-
writes.push("quiet-hours");
|
|
450
|
-
if (instancesWrite)
|
|
451
|
-
writes.push("instances");
|
|
452
|
-
if (capWrite)
|
|
453
|
-
writes.push("cap");
|
|
454
|
-
if (intervalWrite)
|
|
455
|
-
writes.push("min-run-interval");
|
|
456
|
-
if (providerWrite)
|
|
457
|
-
writes.push("provider");
|
|
458
|
-
if (modelWrite)
|
|
459
|
-
writes.push("model");
|
|
460
|
-
if (thinkingWrite)
|
|
461
|
-
writes.push("thinking");
|
|
462
|
-
if (enable)
|
|
463
|
-
writes.push("enable");
|
|
464
|
-
if (disable)
|
|
465
|
-
writes.push("disable");
|
|
466
|
-
if (writes.length > 1)
|
|
467
|
-
fail(`${writes.join(" and ")} writes cannot share one invocation`);
|
|
468
|
-
if (notesWrite) {
|
|
469
|
-
if (clear && rest.includes("--notes-file"))
|
|
470
|
-
fail("give --clear-notes or --notes-file, not both");
|
|
471
|
-
if (rest.includes("--json"))
|
|
472
|
-
fail("--json does not apply to a notes write");
|
|
473
|
-
if (id === DIRECTOR_ROLE)
|
|
474
|
-
fail("the director has no notes");
|
|
475
|
-
requireKnownRole(root, id);
|
|
476
|
-
const text = clear ? "" : readPromptFileArg(notesFile || fail("--notes-file needs a path"));
|
|
477
|
-
try {
|
|
478
|
-
say(`${id}: ${saveRoleNote(roleNotesPath(root, id), text)}`);
|
|
479
|
-
}
|
|
480
|
-
catch (err) {
|
|
481
|
-
fail(errorMessage(err));
|
|
482
|
-
}
|
|
483
|
-
return;
|
|
233
|
+
/** The `tumwater queue --top <n>` branch (PLANS.md): move the Nth queued merge `tumwater
|
|
234
|
+
* queue` lists to the front of the execution order by renaming its queue file to a stamp
|
|
235
|
+
* below the queue's minimum, so it merges next when ready. `cmdQueueCancel`'s twin: `--top
|
|
236
|
+
* <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h tolerated,
|
|
237
|
+
* anything else refused. --role and -f/--follow are refused by name — a live view repaints
|
|
238
|
+
* a read, and position numbers come from the whole queue, whose numbering is
|
|
239
|
+
* `tumwater queue`'s. An entry a live merge marker names for this sha is refused — it is past
|
|
240
|
+
* the queue, so there is nothing to top (unlike `--cancel`, reverting it would undo the change
|
|
241
|
+
* the operator wanted first; the guidance says only that it is already heading in); an
|
|
242
|
+
* out-of-range position fails naming the current depth; position 1 prints the already-first
|
|
243
|
+
* line and exits 0; a concurrently-drained entry prints the gone line and exits 0. */
|
|
244
|
+
async function cmdQueueTop(root, args) {
|
|
245
|
+
const { json, position, rest } = scanPositionArgs(args, "--top", "--top 2");
|
|
246
|
+
// The vocabulary a top invocation accepts is exactly --top and --json, both consumed
|
|
247
|
+
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
248
|
+
if (rest.includes("--role"))
|
|
249
|
+
fail("queue --top moves by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
|
|
250
|
+
if (rest.includes("--follow") || rest.includes("-f"))
|
|
251
|
+
fail("queue --top moves one entry; -f/--follow repaints a live view — drop it");
|
|
252
|
+
rejectUnknownArgs("queue", rest, []);
|
|
253
|
+
if (position === null)
|
|
254
|
+
fail("--top needs a position (usage: tumwater queue --top <n>)");
|
|
255
|
+
await requireReadyRepo(root);
|
|
256
|
+
// Refuse the entry a live merge marker names before touching it: it is mid-flight (being
|
|
257
|
+
// reviewed or merged), so topping it would race the slot that owns it.
|
|
258
|
+
const view = readMergeQueue(root);
|
|
259
|
+
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
260
|
+
const target = view.entries[position - 1];
|
|
261
|
+
if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
|
|
262
|
+
fail(`queue entry ${position} is being merged right now — it's already heading into the merge, so there's nothing to top`);
|
|
263
|
+
}
|
|
264
|
+
const outcome = moveQueuedMergeToTop(root, position);
|
|
265
|
+
if (outcome.status === "out-of-range") {
|
|
266
|
+
fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
|
|
484
267
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
fail("--json does not apply to an instructions write");
|
|
495
|
-
requireKnownRole(root, id);
|
|
496
|
-
const base = baseRoleOf(id);
|
|
497
|
-
if (clearInst) {
|
|
498
|
-
const result = unsetConfigKey(root, `roles.${base}.instructions`);
|
|
499
|
-
if (!result.ok)
|
|
500
|
-
fail(result.error);
|
|
501
|
-
say(`${base}: instructions cleared`);
|
|
268
|
+
if (json) {
|
|
269
|
+
// Every non-failure outcome, `gone` included, exits 0 with a parseable document; the
|
|
270
|
+
// drained entry carries no role/sha/summary, so its JSON names only the position, and
|
|
271
|
+
// an already-first no-op names its own key rather than masquerading as a top.
|
|
272
|
+
if (outcome.status === "gone") {
|
|
273
|
+
sayJson({ gone: { position } });
|
|
274
|
+
}
|
|
275
|
+
else if (outcome.status === "already-first") {
|
|
276
|
+
sayJson({ "already-first": { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
502
277
|
}
|
|
503
278
|
else {
|
|
504
|
-
|
|
505
|
-
const result = setConfigKey(root, `roles.${base}.instructions`, JSON.stringify(text));
|
|
506
|
-
if (!result.ok)
|
|
507
|
-
fail(result.error);
|
|
508
|
-
say(`${base}: instructions saved (${text.length} chars)`);
|
|
279
|
+
sayJson({ topped: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
509
280
|
}
|
|
510
281
|
return;
|
|
511
282
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
// the same setConfigKey/unsetConfigKey core config set/unset use, so checkQuietHours (with
|
|
515
|
-
// the typed key as its label) and validateConfig own validation and the write is atomic.
|
|
516
|
-
// The director is exempt — its quietHoursPerRole.director entry is inert (the gate and
|
|
517
|
-
// doctor both exempt it), like the notes branch's director refusal.
|
|
518
|
-
if (quietWrite) {
|
|
519
|
-
if (clearQuiet && rest.includes("--quiet-hours"))
|
|
520
|
-
fail("give --clear-quiet-hours or --quiet-hours, not both");
|
|
521
|
-
if (rest.includes("--json"))
|
|
522
|
-
fail("--json does not apply to a quiet-hours write");
|
|
523
|
-
requireKnownRole(root, id);
|
|
524
|
-
if (id === DIRECTOR_ROLE)
|
|
525
|
-
fail("the director has no quiet hours");
|
|
526
|
-
const base = baseRoleOf(id);
|
|
527
|
-
const window = flagValue(rest, "--quiet-hours") ?? "";
|
|
528
|
-
writeRoleConfigKey(root, base, clearQuiet, window, `quietHoursPerRole.${base}`, "quiet hours", (v) => JSON.stringify(v), (v) => v);
|
|
283
|
+
if (outcome.status === "gone") {
|
|
284
|
+
say(`queue entry ${position} was already taken by the merge before it could be moved to the front`);
|
|
529
285
|
return;
|
|
530
286
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
// The value lands in maxDailyCostUsdPerRole.<base> through the same
|
|
534
|
-
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so checkDailyBudgetUsd
|
|
535
|
-
// (with the typed key as its label) and validateConfig own validation and the write is
|
|
536
|
-
// atomic. The director is exempt — it has no cap, like the notes and quiet-hours branches'
|
|
537
|
-
// director refusals.
|
|
538
|
-
if (capWrite) {
|
|
539
|
-
if (clearCap && rest.includes("--cap"))
|
|
540
|
-
fail("give --clear-cap or --cap, not both");
|
|
541
|
-
if (rest.includes("--json"))
|
|
542
|
-
fail("--json does not apply to a cap write");
|
|
543
|
-
requireKnownRole(root, id);
|
|
544
|
-
if (id === DIRECTOR_ROLE)
|
|
545
|
-
fail("the director has no daily cap");
|
|
546
|
-
const base = baseRoleOf(id);
|
|
547
|
-
const cap = flagValue(rest, "--cap") ?? "";
|
|
548
|
-
writeRoleConfigKey(root, base, clearCap, cap, `maxDailyCostUsdPerRole.${base}`, "daily cap", (v) => v, (v) => v);
|
|
549
|
-
return;
|
|
287
|
+
if (outcome.status === "already-first") {
|
|
288
|
+
say(`queue entry ${position} is already first: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
550
289
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
// value lands in roles.<base>.minRunIntervalSeconds through the same
|
|
554
|
-
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
|
|
555
|
-
// roles.<id>.minRunIntervalSeconds DURATION_NON_NEGATIVE check owns validation and the
|
|
556
|
-
// write is atomic. Unlike the notes/quiet-hours/cap branches there is no director refusal:
|
|
557
|
-
// the director ticks on the same clock.
|
|
558
|
-
if (intervalWrite) {
|
|
559
|
-
if (clearInterval && rest.includes("--min-run-interval"))
|
|
560
|
-
fail("give --clear-min-run-interval or --min-run-interval, not both");
|
|
561
|
-
if (rest.includes("--json"))
|
|
562
|
-
fail("--json does not apply to a min-run-interval write");
|
|
563
|
-
requireKnownRole(root, id);
|
|
564
|
-
const base = baseRoleOf(id);
|
|
565
|
-
const secs = flagValue(rest, "--min-run-interval") ?? "";
|
|
566
|
-
writeRoleConfigKey(root, base, clearInterval, secs, `roles.${base}.minRunIntervalSeconds`, "min run interval", (v) => v, (v) => `${v}s`);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
// The instances write: the terminal twin of `config set roles.<base>.instances <n>`, the
|
|
570
|
-
// last per-role hold's write. The count lands in roles.<base>.instances through the same
|
|
571
|
-
// setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
|
|
572
|
-
// roles.<id>.instances INSTANCE_COUNT check owns the bound and the write is atomic. As
|
|
573
|
-
// with min run interval there is no director (or non-instance-role) refusal of its own:
|
|
574
|
-
// validateConfig's `!INSTANCE_ROLES.has(id)` branch answers for the director and every
|
|
575
|
-
// custom role, and an unset/empty value clears the key.
|
|
576
|
-
if (instancesWrite) {
|
|
577
|
-
if (clearInstances && rest.includes("--instances"))
|
|
578
|
-
fail("give --clear-instances or --instances, not both");
|
|
579
|
-
if (rest.includes("--json"))
|
|
580
|
-
fail("--json does not apply to an instances write");
|
|
581
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
582
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
583
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
584
|
-
const base = baseRoleOf(id);
|
|
585
|
-
const n = flagValue(rest, "--instances") ?? "";
|
|
586
|
-
writeRoleConfigKey(root, base, clearInstances, n, `roles.${base}.instances`, "instances", (v) => v, (v) => v);
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
// The model-triple writes: the terminal twins of `config set roles.<base>.provider|model|thinking
|
|
590
|
-
// <value>` and `config unset` — the three fields configForRole resolves through seamOverrides.
|
|
591
|
-
// `tumwater role <id>` renders them as its Model: line. Each lands in roles.<base>.<field>.
|
|
592
|
-
// Through setConfigKey/unsetConfigKey, validateConfig's checkModelTripleField ("tier-name"
|
|
593
|
-
// shape) owns validation (model accepts a tier name; thinking must be a THINKING_LEVELS level)
|
|
594
|
-
// and each write is atomic. No director refusal: accepted exactly as --min-run-interval is.
|
|
595
|
-
if (providerWrite) {
|
|
596
|
-
if (clearProvider && rest.includes("--provider"))
|
|
597
|
-
fail("give --clear-provider or --provider, not both");
|
|
598
|
-
if (rest.includes("--json"))
|
|
599
|
-
fail("--json does not apply to a provider write");
|
|
600
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
601
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
602
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
603
|
-
const base = baseRoleOf(id);
|
|
604
|
-
const provider = flagValue(rest, "--provider") ?? "";
|
|
605
|
-
writeRoleConfigKey(root, base, clearProvider, provider, `roles.${base}.provider`, "provider", (v) => JSON.stringify(v), (v) => v);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (modelWrite) {
|
|
609
|
-
if (clearModel && rest.includes("--model"))
|
|
610
|
-
fail("give --clear-model or --model, not both");
|
|
611
|
-
if (rest.includes("--json"))
|
|
612
|
-
fail("--json does not apply to a model write");
|
|
613
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
614
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
615
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
616
|
-
const base = baseRoleOf(id);
|
|
617
|
-
const model = flagValue(rest, "--model") ?? "";
|
|
618
|
-
writeRoleConfigKey(root, base, clearModel, model, `roles.${base}.model`, "model", (v) => JSON.stringify(v), (v) => v);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
if (thinkingWrite) {
|
|
622
|
-
if (clearThinking && rest.includes("--thinking"))
|
|
623
|
-
fail("give --clear-thinking or --thinking, not both");
|
|
624
|
-
if (rest.includes("--json"))
|
|
625
|
-
fail("--json does not apply to a thinking write");
|
|
626
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
627
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
628
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
629
|
-
const base = baseRoleOf(id);
|
|
630
|
-
const level = flagValue(rest, "--thinking") ?? "";
|
|
631
|
-
writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
|
|
632
|
-
return;
|
|
290
|
+
else {
|
|
291
|
+
say(`moved queue entry ${position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
633
292
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
293
|
+
}
|
|
294
|
+
/** `tumwater backlog`: the planned/open entries as Markdown or as the GUI's JSON payload. */
|
|
295
|
+
export async function cmdBacklog(root, args, seams = {}) {
|
|
296
|
+
// No requireReadyRepo gate: the entry readers degrade to [] on a missing file, so the
|
|
297
|
+
// command prints three empty sections in any directory (report's rationale, not config's);
|
|
298
|
+
// the team-workspace refusal still applies.
|
|
299
|
+
// --grep filters the three entry arrays to the matches, the finished-entry lists' one
|
|
300
|
+
// matcher (entryMatches) on the active backlog; it composes with --ready (grep first, so
|
|
301
|
+
// the hidden count and the kept entries count only the matches) and with --json. The
|
|
302
|
+
// missing-value wording lives in BACKLOG_GREP_VALUE_ERROR, the flag gate's and this
|
|
303
|
+
// parser's shared string. -f/--follow repaints the same view every couple of seconds
|
|
304
|
+
// until Ctrl+C or the parent process exits, composing with --ready and --grep; it refuses
|
|
305
|
+
// --json (a live view is one repainted document, not one snapshot).
|
|
306
|
+
const BACKLOG_GREP_VALUE_ERROR = "backlog --grep needs text (usage: tumwater backlog --grep <text>)";
|
|
307
|
+
rejectUnknownArgs("backlog", args, [JSON_FLAG, READY_FLAG, grepFlagSpec(BACKLOG_GREP_VALUE_ERROR), FOLLOW_FLAG]);
|
|
308
|
+
refuseTeamWorkspace(root);
|
|
309
|
+
const { rest, pattern } = parseGrepFlag(args, BACKLOG_GREP_VALUE_ERROR);
|
|
310
|
+
const ready = rest.includes("--ready");
|
|
311
|
+
const follow = hasFollowFlag(rest);
|
|
312
|
+
if (follow && rest.includes("--json"))
|
|
313
|
+
fail("backlog --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
314
|
+
// Machine-readable backlog — the three entry arrays the Markdown view renders and the
|
|
315
|
+
// GUI's /api/backlog serves (status --json's "print the endpoint's payload" pattern),
|
|
316
|
+
// plus `holds.plans`/`holds.bugs` parallel to the entry arrays so a skipped entry's
|
|
317
|
+
// reason is data, not inference: a pretty-printed JSON document in every exit-0 case,
|
|
318
|
+
// never prose. The payload is read once per branch (snapshot or frame) so the Markdown
|
|
319
|
+
// renderer and the JSON document consume the same arrays.
|
|
320
|
+
// --ready filters both branches through readyBacklog; the Markdown branch adds a count of
|
|
321
|
+
// the held entries it hid. The --json/--ready reads go over `rest` (the --grep value token
|
|
322
|
+
// removed), so a flag-shaped pattern like `--grep --json` greps for "--json" rather than
|
|
323
|
+
// flipping this branch into JSON mode.
|
|
324
|
+
const readBacklog = () => {
|
|
325
|
+
const view = { ...backlogPayload(root), holds: backlogHolds(root), inProgress: backlogInProgress(root) };
|
|
326
|
+
const filtered = pattern === null ? view : grepBacklog(view, pattern);
|
|
327
|
+
if (!ready)
|
|
328
|
+
return { view: filtered, hidden: 0 };
|
|
329
|
+
const kept = readyBacklog(filtered);
|
|
330
|
+
return { view: kept, hidden: filtered.plans.length + filtered.bugs.length - kept.plans.length - kept.bugs.length };
|
|
331
|
+
};
|
|
332
|
+
const renderBacklogText = (r) => renderBacklogMarkdown(r.view) + (r.hidden > 0
|
|
333
|
+
? `\n\n_(${r.hidden} held entries hidden; run \`tumwater backlog\` to see them)_` : "");
|
|
334
|
+
if (follow) {
|
|
335
|
+
await followView(() => renderBacklogText(readBacklog()), seams);
|
|
653
336
|
return;
|
|
654
337
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
338
|
+
const result = readBacklog();
|
|
339
|
+
sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
|
|
340
|
+
}
|
|
341
|
+
// `tumwater quarantine [--json]` — the read-only view of the review gate's flake
|
|
342
|
+
// quarantine. No requireReadyRepo gate: loadQuarantine degrades an absent or malformed
|
|
343
|
+
// file to the empty list (the cmdBacklog rationale), so the command prints the empty line
|
|
344
|
+
// in any directory, and it never writes quarantine.json (a test leaves the list only
|
|
345
|
+
// through releaseQuarantine). --json serves the same positioned rows to scripts.
|
|
346
|
+
export async function cmdQuarantine(root, args) {
|
|
347
|
+
rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
|
|
348
|
+
refuseTeamWorkspace(root);
|
|
349
|
+
const rows = quarantineRows(loadQuarantine(root));
|
|
350
|
+
sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
|
|
658
351
|
}
|