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,128 +1,67 @@
|
|
|
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, parsePositionFlag, parseRoleListScope, rejectRepeatFlag } 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 { collectTeamChanges, collectRoleChange } from "../change/change-data.js";
|
|
20
|
-
import { renderTeamChange, renderRoleChange } from "../change/change-render.js";
|
|
18
|
+
import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
|
|
19
|
+
import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, collectRoleChanges, normalizeDiffPath } from "../change/change-data.js";
|
|
20
|
+
import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
|
|
21
21
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
22
|
-
import { renderMergeQueue } from "../merge/merge-queue-render.js";
|
|
22
|
+
import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.js";
|
|
23
|
+
import { mergeChanges } from "../merge/merge-slot.js";
|
|
24
|
+
import { shortSha } from "../text/format.js";
|
|
25
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
23
26
|
import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
24
27
|
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]);
|
|
28
|
+
import { roleInScope } from "../roles/runner-ids.js";
|
|
29
|
+
/** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON
|
|
30
|
+
* document GET /api/status serves. `-f/--follow` repaints the table every couple of seconds
|
|
31
|
+
* until Ctrl+C or the process that started it exits, so a backgrounded follow does not
|
|
32
|
+
* outlive its parent. */
|
|
33
|
+
export async function cmdStatus(root, args, seams = {}) {
|
|
34
|
+
rejectUnknownArgs("status", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
|
|
35
|
+
const follow = hasFollowFlag(args);
|
|
36
|
+
if (follow && args.includes("--json"))
|
|
37
|
+
fail("status --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
111
38
|
await requireReadyRepo(root);
|
|
39
|
+
const scope = parseRoleListScope(root, args);
|
|
40
|
+
if (follow) {
|
|
41
|
+
await followStatus(root, scope, seams);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
112
44
|
if (args.includes("--json")) {
|
|
113
45
|
// Machine-readable team state — the document GET /api/status serves minus the
|
|
114
46
|
// serving process's own `serverBuildSha`, printed with no server. A query, not a
|
|
115
47
|
// health verdict: exit 0 on any successful read and let scripts interpret fields
|
|
116
48
|
// themselves ("running": false is data, not failure).
|
|
117
|
-
sayJson(statusPayload(root));
|
|
49
|
+
sayJson(statusPayload(root, Date.now(), scope));
|
|
118
50
|
}
|
|
119
51
|
else {
|
|
120
|
-
|
|
52
|
+
const snap = snapshot(root);
|
|
53
|
+
say(renderStatus(root, scope ? scopeSnapshotToRoles(snap, scope) : snap, process.stdout.isTTY ? process.stdout.columns : undefined, scope !== null));
|
|
121
54
|
}
|
|
122
55
|
}
|
|
123
|
-
/** `tumwater
|
|
124
|
-
|
|
125
|
-
|
|
56
|
+
/** `tumwater status -f`'s live loop: the team snapshot repainted until the parent exits. */
|
|
57
|
+
async function followStatus(root, scope, seams) {
|
|
58
|
+
const tty = seams.tty ?? process.stdout.isTTY;
|
|
59
|
+
await followView(() => renderStatus(root, scope ? scopeSnapshotToRoles(snapshot(root), scope) : snapshot(root), tty ? process.stdout.columns : undefined, scope !== null), seams);
|
|
60
|
+
}
|
|
61
|
+
/** `tumwater diff`: one line per role holding pending work, the files two or more roles touch
|
|
62
|
+
* with `--overlaps` (`--role` keeps those a listed role holds, `--path` those at or under it),
|
|
63
|
+
* or the full per-role view of each role listed with `--role <id>[,<id>...]`. */
|
|
64
|
+
export async function cmdDiff(root, args, seams = {}) {
|
|
126
65
|
// The repo half of requireReadyRepo still gates: a directory tumwater cannot read yet
|
|
127
66
|
// (no git, not a repository, no tumwater.json, no commits) has no team to ask about,
|
|
128
67
|
// and the change view's own degradation would misreport that as "main branch <name>
|
|
@@ -130,529 +69,308 @@ export async function cmdDiff(root, args) {
|
|
|
130
69
|
// command. Past the gate an absent worktree still degrades to a `no worktree for
|
|
131
70
|
// <role>` line (exit 0), so the command answers in any initialized directory —
|
|
132
71
|
// report's rationale.
|
|
133
|
-
rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG]);
|
|
72
|
+
rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG, OVERLAPS_FLAG, DIFF_PATH_FLAG, FOLLOW_FLAG]);
|
|
134
73
|
await requireReadyRepo(root);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
74
|
+
rejectRepeatFlag(args.filter((a) => a === "--path").length > 1, "--path");
|
|
75
|
+
const follow = hasFollowFlag(args);
|
|
76
|
+
if (follow && args.includes("--json"))
|
|
77
|
+
fail("diff --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
78
|
+
if (args.includes("--overlaps")) {
|
|
140
79
|
if (args.includes("--stat"))
|
|
141
|
-
fail("diff --
|
|
142
|
-
|
|
80
|
+
fail("diff --overlaps prints paths, not patches; drop --stat");
|
|
81
|
+
// Overlaps are computed across every role; --role only filters which entries print.
|
|
82
|
+
const scope = parseRoleListScope(root, args);
|
|
83
|
+
const path = diffPathArg(args);
|
|
84
|
+
// The read and its zero-arg render are factored into one thunk so the follow's frame
|
|
85
|
+
// and the snapshot print exactly the same text from the same reader.
|
|
86
|
+
const readOverlaps = async () => {
|
|
87
|
+
let view = await collectChangeOverlaps(root);
|
|
88
|
+
if (scope === null && path === undefined)
|
|
89
|
+
return { view, render: () => renderChangeOverlaps(view) };
|
|
90
|
+
if (scope !== null)
|
|
91
|
+
view = scopeChangeOverlaps(view, scope);
|
|
92
|
+
if (path !== undefined)
|
|
93
|
+
view = scopeChangeOverlapsToPath(view, path);
|
|
94
|
+
return { view, render: () => renderChangeOverlaps(view, scope ?? undefined, path) };
|
|
95
|
+
};
|
|
96
|
+
if (follow) {
|
|
97
|
+
await followView(async () => (await readOverlaps()).render(), seams);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { view, render } = await readOverlaps();
|
|
101
|
+
sayJsonOrRender(args, view, () => render());
|
|
102
|
+
return;
|
|
143
103
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
104
|
+
// Absent --role is the team-wide form: one line per role holding pending work
|
|
105
|
+
// (parseRoleListScope returns null only for an absent flag — an empty or unknown piece
|
|
106
|
+
// already failed above). One named role keeps the single per-role view; a list prints each
|
|
107
|
+
// listed role's view in order.
|
|
108
|
+
const roles = parseRoleListScope(root, args);
|
|
109
|
+
const path = roles === null ? undefined : diffPathArg(args);
|
|
110
|
+
// The roster/role reads below are factored into a thunk (view + zero-arg render) so the
|
|
111
|
+
// follow's frame and the snapshot share one reader and print byte-identical text.
|
|
112
|
+
const readChange = async () => {
|
|
113
|
+
if (roles === null) {
|
|
114
|
+
if (args.includes("--stat"))
|
|
115
|
+
fail("diff --stat needs --role <id> (the team roster prints no patch)");
|
|
116
|
+
if (args.includes("--path"))
|
|
117
|
+
fail("diff --path needs --role <id> (the team roster prints no patch)");
|
|
118
|
+
const view = await collectTeamChanges(root);
|
|
119
|
+
return { view, render: () => renderTeamChange(view) };
|
|
120
|
+
}
|
|
121
|
+
if (roles.length === 1) {
|
|
122
|
+
const change = await collectRoleChange(root, roles[0], { stat: args.includes("--stat"), path });
|
|
123
|
+
return { view: change, render: () => renderRoleChange(change) };
|
|
124
|
+
}
|
|
125
|
+
const view = await collectRoleChanges(root, roles, { stat: args.includes("--stat"), path });
|
|
126
|
+
return { view, render: () => renderRoleChanges(view) };
|
|
127
|
+
};
|
|
128
|
+
if (follow) {
|
|
129
|
+
await followView(async () => (await readChange()).render(), seams);
|
|
130
|
+
return;
|
|
147
131
|
}
|
|
132
|
+
const { view, render } = await readChange();
|
|
133
|
+
sayJsonOrRender(args, view, () => render());
|
|
134
|
+
}
|
|
135
|
+
/** The normalised `--path` value of `diff --role` (undefined when absent): leading `./` pieces
|
|
136
|
+
* and trailing `/` characters stripped, failing on an empty, absolute or `..`-bearing result. */
|
|
137
|
+
function diffPathArg(args) {
|
|
138
|
+
const raw = flagValue(args, "--path");
|
|
139
|
+
if (raw === null || raw === undefined)
|
|
140
|
+
return undefined;
|
|
141
|
+
const norm = normalizeDiffPath(raw);
|
|
142
|
+
if (norm === null)
|
|
143
|
+
fail("diff --path takes a repo-relative path such as src/cli.ts (as diff --overlaps prints it)");
|
|
144
|
+
return norm;
|
|
148
145
|
}
|
|
149
|
-
/** `tumwater queue`: the durable merge queue in execution order, as text or as
|
|
150
|
-
* `{entries, unreadable}` document.
|
|
151
|
-
|
|
146
|
+
/** `tumwater queue [-f/--follow]`: the durable merge queue in execution order, as text or as
|
|
147
|
+
* the `{entries, unreadable}` document. `-f/--follow` repaints the queue every couple of
|
|
148
|
+
* seconds until Ctrl+C or the process that started it exits, so a backgrounded follow does
|
|
149
|
+
* not outlive its parent (the same rule `status -f` uses). */
|
|
150
|
+
export async function cmdQueue(root, args, seams = {}) {
|
|
151
|
+
// `tumwater queue --cancel <n>` is the read-only command's one destructive mode: route it
|
|
152
|
+
// to its own branch before the read gate, so --cancel never leaks into the view parsing.
|
|
153
|
+
if (args.includes("--cancel")) {
|
|
154
|
+
await cmdQueueCancel(root, args);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (args.includes("--top")) {
|
|
158
|
+
await cmdQueueTop(root, args);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
152
161
|
// The repo half of requireReadyRepo gates: like every repo-bound query, a directory
|
|
153
162
|
// tumwater cannot read yet has no queue to show. Past the gate an absent queue dir reads
|
|
154
163
|
// as an empty queue (a `merge queue empty` line, exit 0), so no merge ever queued is
|
|
155
164
|
// data, not an error.
|
|
156
|
-
rejectUnknownArgs("queue", args, [JSON_FLAG]);
|
|
165
|
+
rejectUnknownArgs("queue", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
|
|
157
166
|
await requireReadyRepo(root);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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)_` : ""));
|
|
167
|
+
const roles = parseRoleListScope(root, args);
|
|
168
|
+
const follow = hasFollowFlag(args);
|
|
169
|
+
if (follow && args.includes("--json"))
|
|
170
|
+
fail("queue --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
171
|
+
// Scoped read shared by the follow and the one-shot branches, so the render's age and the
|
|
172
|
+
// in-flight marker share one clock read and each frame re-reads the queue. --role keeps
|
|
173
|
+
// the listed roles' entries (a base id taking its instances too) under their whole-queue
|
|
174
|
+
// positions; the in-flight marker is judged against the kept entries only, and
|
|
175
|
+
// `unreadable` stays whole because an unreadable file has no role to attribute it to.
|
|
176
|
+
const readQueue = () => {
|
|
177
|
+
const whole = readMergeQueue(root);
|
|
178
|
+
const scoped = roles ? scopeMergeQueue(whole, roles, (r) => roleInScope(r, roles)) : undefined;
|
|
179
|
+
const view = scoped ? scoped.view : whole;
|
|
180
|
+
return { view, inFlight: queueInFlight(root, view.entries, orchestratorAlive(root)), scope: scoped?.scope };
|
|
181
|
+
};
|
|
182
|
+
if (follow) {
|
|
183
|
+
await followView(() => {
|
|
184
|
+
const { view, inFlight, scope } = readQueue();
|
|
185
|
+
return renderMergeQueue(view, Date.now(), inFlight, scope);
|
|
186
|
+
}, seams);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const { view, inFlight, scope } = readQueue();
|
|
190
|
+
sayJsonOrRender(args, inFlight ? { ...view, inFlight } : view, () => renderMergeQueue(view, Date.now(), inFlight, scope));
|
|
192
191
|
}
|
|
193
|
-
/** `tumwater
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
192
|
+
/** The `tumwater queue --cancel <n>` branch (PLANS.md): remove the Nth queued merge `tumwater
|
|
193
|
+
* queue` lists and delete its role's merge pin, so next-run leftover recovery cannot silently
|
|
194
|
+
* re-enqueue the change. Parsed as cancelAndAnnounce parses `--cancel` (backlog-cli.ts):
|
|
195
|
+
* `--cancel <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h
|
|
196
|
+
* tolerated, anything else refused. --role and -f/--follow are refused by name — a live view
|
|
197
|
+
* repaints a read, and position numbers come from the whole queue, whose numbering is
|
|
198
|
+
* `tumwater queue`'s. An entry a live merge marker names for this sha is refused (wait for it
|
|
199
|
+
* to land, then revert); an out-of-range position fails naming the current depth. */
|
|
200
|
+
async function cmdQueueCancel(root, args) {
|
|
201
|
+
let position = null;
|
|
202
|
+
let json = false;
|
|
203
|
+
const rest = [];
|
|
219
204
|
for (let i = 0; i < args.length; i++) {
|
|
220
205
|
const arg = args[i];
|
|
221
|
-
if (
|
|
222
|
-
rejectRepeatFlag(
|
|
223
|
-
|
|
206
|
+
if (arg === "--json") {
|
|
207
|
+
rejectRepeatFlag(json, "--json");
|
|
208
|
+
json = true;
|
|
224
209
|
continue;
|
|
225
210
|
}
|
|
226
|
-
if (
|
|
227
|
-
rejectRepeatFlag(
|
|
228
|
-
|
|
211
|
+
if (arg === "--cancel") {
|
|
212
|
+
rejectRepeatFlag(position !== null, "--cancel");
|
|
213
|
+
position = parsePositionFlag("--cancel", args[i + 1], "--cancel 2");
|
|
214
|
+
i++;
|
|
229
215
|
continue;
|
|
230
216
|
}
|
|
231
|
-
if (
|
|
232
|
-
rejectRepeatFlag(limit !== undefined, "-n");
|
|
233
|
-
limit = parseCountFlag("-n", args[++i]);
|
|
217
|
+
if (arg === "--help" || arg === "-h")
|
|
234
218
|
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));
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
const q = openQuestionAt(root, show);
|
|
295
|
-
if (q === null) {
|
|
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));
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (answeredFlag) {
|
|
307
|
-
if (words.length > 0)
|
|
308
|
-
fail("--answered lists answered questions; it takes no subcommand");
|
|
309
|
-
if (file !== undefined)
|
|
310
|
-
fail("--file only applies to questions answer <n>");
|
|
311
|
-
if (jsonFlag)
|
|
312
|
-
sayJson(answeredQuestionPayload(root, limit ?? 10, grep));
|
|
313
|
-
else
|
|
314
|
-
say(answeredQuestionList(root, limit ?? 10, grep));
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (words.length === 0 && file !== undefined)
|
|
318
|
-
fail("--file only applies to questions answer <n>");
|
|
319
|
-
if (words.length === 0) {
|
|
320
|
-
sayQuestionList(root, jsonFlag);
|
|
321
|
-
return;
|
|
219
|
+
rest.push(arg);
|
|
322
220
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
221
|
+
// The vocabulary a cancel invocation accepts is exactly --cancel and --json, both consumed
|
|
222
|
+
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
223
|
+
if (rest.includes("--role"))
|
|
224
|
+
fail("queue --cancel cancels by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
|
|
225
|
+
if (rest.includes("--follow") || rest.includes("-f"))
|
|
226
|
+
fail("queue --cancel removes one entry; -f/--follow repaints a live view — drop it");
|
|
227
|
+
rejectUnknownArgs("queue", rest, []);
|
|
228
|
+
if (position === null)
|
|
229
|
+
fail("--cancel needs a position (usage: tumwater queue --cancel <n>)");
|
|
230
|
+
await requireReadyRepo(root);
|
|
231
|
+
// Refuse the entry a live merge marker names before touching it: it is mid-flight (being
|
|
232
|
+
// reviewed or merged), so cancelling it would race the slot that owns it.
|
|
233
|
+
const view = readMergeQueue(root);
|
|
234
|
+
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
235
|
+
const target = view.entries[position - 1];
|
|
236
|
+
if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
|
|
237
|
+
fail(`queue entry ${position} is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(target.sha)}\``);
|
|
337
238
|
}
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
const question = open[n - 1];
|
|
342
|
-
if (question === undefined) {
|
|
343
|
-
const range = open.length > 0 ? ` — valid: 1-${open.length}` : "";
|
|
344
|
-
fail(`no question at position ${n} (${open.length} open)${range}`);
|
|
345
|
-
}
|
|
346
|
-
const edited = decisionFromEditorBuffer(editInEditor(answerEditorBuffer(question.title, question.body), `answer-${n}.md`, "pass a decision or --file <path>"));
|
|
347
|
-
if (edited.trim() === "")
|
|
348
|
-
fail("questions answer needs a decision: the editor left it blank; nothing changed");
|
|
349
|
-
const answered = answerQuestionFromText(root, n, edited, question.title);
|
|
350
|
-
await commitAndReportAnswer(root, n, jsonFlag, answered.title, answered.decision);
|
|
351
|
-
return;
|
|
239
|
+
const outcome = await cancelQueuedMerge(root, position);
|
|
240
|
+
if (outcome.status === "out-of-range") {
|
|
241
|
+
fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
|
|
352
242
|
}
|
|
353
|
-
if (
|
|
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);
|
|
367
|
-
}
|
|
368
|
-
/** Prose-mode follow-up to `questions answer`: wake the asking role and print the confirmation. */
|
|
369
|
-
function sayAskerWake(root, title) {
|
|
370
|
-
const wake = wakeQuestionAsker(root, title);
|
|
371
|
-
if (wake !== null)
|
|
372
|
-
say(wake);
|
|
373
|
-
}
|
|
374
|
-
/** Refuse an unknown role id with the shared unknownRoleMessage wording before a per-role
|
|
375
|
-
* config write resolves the id. Every `tumwater role` write branch starts with the same
|
|
376
|
-
* prelude — load the config (falling back to defaultConfig so built-in roles resolve) and
|
|
377
|
-
* throw the unknown-role error unless the id appears — so this keeps that check in one place.
|
|
378
|
-
* The caller then derives `baseRoleOf(id)` for the write key. */
|
|
379
|
-
function requireKnownRole(root, id) {
|
|
380
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
381
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
382
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
383
|
-
}
|
|
384
|
-
/** The config-key write the six per-role hold branches (quiet hours, daily cap, min run
|
|
385
|
-
* interval, provider, model and thinking) share: with `clear`, or an empty typed value, unset
|
|
386
|
-
* `key` under `base`; else set it. `serialize` shapes the value `setConfigKey` stores (it
|
|
387
|
-
* JSON-encodes nothing itself) and `display` gives the human phrase for the confirmation
|
|
388
|
-
* line. The caller returns right after. The instructions branch keeps its own block: its
|
|
389
|
-
* value is a file's text, so an empty value is a legitimate write, not a clear. */
|
|
390
|
-
function writeRoleConfigKey(root, base, clear, raw, key, noun, serialize, display) {
|
|
391
|
-
if (clear || raw.trim() === "") {
|
|
392
|
-
const result = unsetConfigKey(root, key);
|
|
393
|
-
if (!result.ok)
|
|
394
|
-
fail(result.error);
|
|
395
|
-
say(`${base}: ${noun} cleared`);
|
|
243
|
+
if (json) {
|
|
244
|
+
sayJson({ cancelled: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
396
245
|
}
|
|
397
246
|
else {
|
|
398
|
-
|
|
399
|
-
if (!result.ok)
|
|
400
|
-
fail(result.error);
|
|
401
|
-
say(`${base}: ${noun} set to ${display(raw)}`);
|
|
247
|
+
say(`cancelled queue entry ${position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
402
248
|
}
|
|
403
249
|
}
|
|
404
|
-
/** `tumwater
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const clearQuiet = rest.includes("--clear-quiet-hours");
|
|
426
|
-
const clearCap = rest.includes("--clear-cap");
|
|
427
|
-
const clearInterval = rest.includes("--clear-min-run-interval");
|
|
428
|
-
const clearInstances = rest.includes("--clear-instances");
|
|
429
|
-
const clearProvider = rest.includes("--clear-provider");
|
|
430
|
-
const clearModel = rest.includes("--clear-model");
|
|
431
|
-
const clearThinking = rest.includes("--clear-thinking");
|
|
432
|
-
const notesWrite = clear || rest.includes("--notes-file");
|
|
433
|
-
const instructionsWrite = clearInst || rest.includes("--instructions-file");
|
|
434
|
-
const quietWrite = clearQuiet || rest.includes("--quiet-hours");
|
|
435
|
-
const capWrite = clearCap || rest.includes("--cap");
|
|
436
|
-
const intervalWrite = clearInterval || rest.includes("--min-run-interval");
|
|
437
|
-
const instancesWrite = clearInstances || rest.includes("--instances");
|
|
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)}`);
|
|
250
|
+
/** The `tumwater queue --top <n>` branch (PLANS.md): move the Nth queued merge `tumwater
|
|
251
|
+
* queue` lists to the front of the execution order by renaming its queue file to a stamp
|
|
252
|
+
* below the queue's minimum, so it merges next when ready. `cmdQueueCancel`'s twin: `--top
|
|
253
|
+
* <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h tolerated,
|
|
254
|
+
* anything else refused. --role and -f/--follow are refused by name — a live view repaints
|
|
255
|
+
* a read, and position numbers come from the whole queue, whose numbering is
|
|
256
|
+
* `tumwater queue`'s. An entry a live merge marker names for this sha is refused — it is past
|
|
257
|
+
* the queue, so there is nothing to top (unlike `--cancel`, reverting it would undo the change
|
|
258
|
+
* the operator wanted first; the guidance says only that it is already heading in); an
|
|
259
|
+
* out-of-range position fails naming the current depth; position 1 prints the already-first
|
|
260
|
+
* line and exits 0; a concurrently-drained entry prints the gone line and exits 0. */
|
|
261
|
+
async function cmdQueueTop(root, args) {
|
|
262
|
+
let position = null;
|
|
263
|
+
let json = false;
|
|
264
|
+
const rest = [];
|
|
265
|
+
for (let i = 0; i < args.length; i++) {
|
|
266
|
+
const arg = args[i];
|
|
267
|
+
if (arg === "--json") {
|
|
268
|
+
rejectRepeatFlag(json, "--json");
|
|
269
|
+
json = true;
|
|
270
|
+
continue;
|
|
479
271
|
}
|
|
480
|
-
|
|
481
|
-
|
|
272
|
+
if (arg === "--top") {
|
|
273
|
+
rejectRepeatFlag(position !== null, "--top");
|
|
274
|
+
position = parsePositionFlag("--top", args[i + 1], "--top 2");
|
|
275
|
+
i++;
|
|
276
|
+
continue;
|
|
482
277
|
}
|
|
483
|
-
|
|
278
|
+
if (arg === "--help" || arg === "-h")
|
|
279
|
+
continue;
|
|
280
|
+
rest.push(arg);
|
|
484
281
|
}
|
|
485
|
-
// The
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
282
|
+
// The vocabulary a top invocation accepts is exactly --top and --json, both consumed
|
|
283
|
+
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
284
|
+
if (rest.includes("--role"))
|
|
285
|
+
fail("queue --top moves by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
|
|
286
|
+
if (rest.includes("--follow") || rest.includes("-f"))
|
|
287
|
+
fail("queue --top moves one entry; -f/--follow repaints a live view — drop it");
|
|
288
|
+
rejectUnknownArgs("queue", rest, []);
|
|
289
|
+
if (position === null)
|
|
290
|
+
fail("--top needs a position (usage: tumwater queue --top <n>)");
|
|
291
|
+
await requireReadyRepo(root);
|
|
292
|
+
// Refuse the entry a live merge marker names before touching it: it is mid-flight (being
|
|
293
|
+
// reviewed or merged), so topping it would race the slot that owns it.
|
|
294
|
+
const view = readMergeQueue(root);
|
|
295
|
+
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
296
|
+
const target = view.entries[position - 1];
|
|
297
|
+
if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
|
|
298
|
+
fail(`queue entry ${position} is being merged right now — it's already heading into the merge, so there's nothing to top`);
|
|
299
|
+
}
|
|
300
|
+
const outcome = moveQueuedMergeToTop(root, position);
|
|
301
|
+
if (outcome.status === "out-of-range") {
|
|
302
|
+
fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
|
|
303
|
+
}
|
|
304
|
+
if (json) {
|
|
305
|
+
// Every non-failure outcome, `gone` included, exits 0 with a parseable document; the
|
|
306
|
+
// drained entry carries no role/sha/summary, so its JSON names only the position, and
|
|
307
|
+
// an already-first no-op names its own key rather than masquerading as a top.
|
|
308
|
+
if (outcome.status === "gone") {
|
|
309
|
+
sayJson({ gone: { position } });
|
|
310
|
+
}
|
|
311
|
+
else if (outcome.status === "already-first") {
|
|
312
|
+
sayJson({ "already-first": { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
502
313
|
}
|
|
503
314
|
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)`);
|
|
315
|
+
sayJson({ topped: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
|
|
509
316
|
}
|
|
510
317
|
return;
|
|
511
318
|
}
|
|
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);
|
|
319
|
+
if (outcome.status === "gone") {
|
|
320
|
+
say(`queue entry ${position} was already taken by the merge before it could be moved to the front`);
|
|
529
321
|
return;
|
|
530
322
|
}
|
|
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;
|
|
550
|
-
}
|
|
551
|
-
// The min-run-interval write: the terminal twin of `config set
|
|
552
|
-
// roles.<base>.minRunIntervalSeconds <seconds>`, the fourth per-role hold's write. The
|
|
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;
|
|
323
|
+
if (outcome.status === "already-first") {
|
|
324
|
+
say(`queue entry ${position} is already first: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
620
325
|
}
|
|
621
|
-
|
|
622
|
-
|
|
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;
|
|
326
|
+
else {
|
|
327
|
+
say(`moved queue entry ${position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
|
|
633
328
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
329
|
+
}
|
|
330
|
+
/** `tumwater backlog`: the planned/open entries as Markdown or as the GUI's JSON payload. */
|
|
331
|
+
export async function cmdBacklog(root, args, seams = {}) {
|
|
332
|
+
// No requireReadyRepo gate: the entry readers degrade to [] on a missing file, so the
|
|
333
|
+
// command prints three empty sections in any directory (report's rationale, not config's);
|
|
334
|
+
// the team-workspace refusal still applies.
|
|
335
|
+
// --grep filters the three entry arrays to the matches, the finished-entry lists' one
|
|
336
|
+
// matcher (entryMatches) on the active backlog; it composes with --ready (grep first, so
|
|
337
|
+
// the hidden count and the kept entries count only the matches) and with --json. The
|
|
338
|
+
// missing-value wording lives in BACKLOG_GREP_VALUE_ERROR, the flag gate's and this
|
|
339
|
+
// parser's shared string. -f/--follow repaints the same view every couple of seconds
|
|
340
|
+
// until Ctrl+C or the parent process exits, composing with --ready and --grep; it refuses
|
|
341
|
+
// --json (a live view is one repainted document, not one snapshot).
|
|
342
|
+
const BACKLOG_GREP_VALUE_ERROR = "backlog --grep needs text (usage: tumwater backlog --grep <text>)";
|
|
343
|
+
rejectUnknownArgs("backlog", args, [JSON_FLAG, READY_FLAG, grepFlagSpec(BACKLOG_GREP_VALUE_ERROR), FOLLOW_FLAG]);
|
|
344
|
+
refuseTeamWorkspace(root);
|
|
345
|
+
const { rest, pattern } = parseGrepFlag(args, BACKLOG_GREP_VALUE_ERROR);
|
|
346
|
+
const ready = rest.includes("--ready");
|
|
347
|
+
const follow = hasFollowFlag(rest);
|
|
348
|
+
if (follow && rest.includes("--json"))
|
|
349
|
+
fail("backlog --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
350
|
+
// Machine-readable backlog — the three entry arrays the Markdown view renders and the
|
|
351
|
+
// GUI's /api/backlog serves (status --json's "print the endpoint's payload" pattern),
|
|
352
|
+
// plus `holds.plans`/`holds.bugs` parallel to the entry arrays so a skipped entry's
|
|
353
|
+
// reason is data, not inference: a pretty-printed JSON document in every exit-0 case,
|
|
354
|
+
// never prose. The payload is read once per branch (snapshot or frame) so the Markdown
|
|
355
|
+
// renderer and the JSON document consume the same arrays.
|
|
356
|
+
// --ready filters both branches through readyBacklog; the Markdown branch adds a count of
|
|
357
|
+
// the held entries it hid. The --json/--ready reads go over `rest` (the --grep value token
|
|
358
|
+
// removed), so a flag-shaped pattern like `--grep --json` greps for "--json" rather than
|
|
359
|
+
// flipping this branch into JSON mode.
|
|
360
|
+
const readBacklog = () => {
|
|
361
|
+
const view = { ...backlogPayload(root), holds: backlogHolds(root), inProgress: backlogInProgress(root) };
|
|
362
|
+
const filtered = pattern === null ? view : grepBacklog(view, pattern);
|
|
363
|
+
if (!ready)
|
|
364
|
+
return { view: filtered, hidden: 0 };
|
|
365
|
+
const kept = readyBacklog(filtered);
|
|
366
|
+
return { view: kept, hidden: filtered.plans.length + filtered.bugs.length - kept.plans.length - kept.bugs.length };
|
|
367
|
+
};
|
|
368
|
+
const renderBacklogText = (r) => renderBacklogMarkdown(r.view) + (r.hidden > 0
|
|
369
|
+
? `\n\n_(${r.hidden} held entries hidden; run \`tumwater backlog\` to see them)_` : "");
|
|
370
|
+
if (follow) {
|
|
371
|
+
await followView(() => renderBacklogText(readBacklog()), seams);
|
|
653
372
|
return;
|
|
654
373
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
|
|
374
|
+
const result = readBacklog();
|
|
375
|
+
sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
|
|
658
376
|
}
|