tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,63 +1,67 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../
|
|
5
|
+
import { cancelScheduledStop, isTeamPaused, scheduleStop } from "../team/team-state.js";
|
|
6
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
6
7
|
import { durationLabel } from "../cli/cli-args.js";
|
|
7
|
-
import { formatDate, formatTime,
|
|
8
|
+
import { formatDate, formatTime, humanUntil } from "../text/datetime.js";
|
|
8
9
|
import { submitRolePrompt } from "../inbox/inbox-submit.js";
|
|
10
|
+
import { questionAsker } from "../backlog/question-answer.js";
|
|
11
|
+
import { knownRoleIdsCached } from "../config/config.js";
|
|
9
12
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
10
13
|
import { loadLoopState, saveLoopState, zeroCounters } from "../loop/loop-state.js";
|
|
11
14
|
import { clearBackoff } from "../scheduling/backoff.js";
|
|
12
15
|
import { writeJsonAtomic } from "../files/json-files.js";
|
|
13
|
-
import { reclaimPass } from "../
|
|
16
|
+
import { reclaimPass } from "../team/reclaim.js";
|
|
14
17
|
import { formatGB } from "../text/format.js";
|
|
18
|
+
import { plural } from "../text/phrases.js";
|
|
15
19
|
import { cancelScheduledWakes, pendingScheduledWakes } from "./operator-requests.js";
|
|
16
20
|
import { abortRequestPath, reclaimRequestPath, resetRequestPath, restartRequestPath, scheduledWakeDir, wakeRequestPath, } from "../paths.js";
|
|
17
21
|
/** The marker-writing cores of the operator-intent protocol, shared by every surface that writes
|
|
18
22
|
* one (the `cmd*` CLI commands in src/operator/operator-commands.ts, the dashboard's POST routes
|
|
19
23
|
* in src/gui/gui-endpoint-commands.ts, and the TUI's key bindings in src/ui/tui/tui-keys.ts): each
|
|
20
24
|
* core returns the confirmation its caller prints verbatim, so the surfaces cannot drift on marker
|
|
21
|
-
* format, idempotence, or wording. The
|
|
25
|
+
* format, idempotence, or wording. The team-side consumer half is
|
|
22
26
|
* src/operator/operator-requests.ts; this module holds the producer half's shared core, split out
|
|
23
27
|
* of operator/operator-commands.ts so the UI layer never depends on the CLI's arg parsing. */
|
|
24
28
|
/** The error every live-harness-only command reports (abort, stop): one literal so the two
|
|
25
29
|
* surfaces cannot drift on the message an operator sees when nothing is running. */
|
|
26
|
-
export const NO_HARNESS_ERROR = "
|
|
30
|
+
export const NO_HARNESS_ERROR = "tumwater is not running — start it with `tumwater start` first";
|
|
27
31
|
/** How a marker command reports when its marker takes effect, derived from one liveness
|
|
28
|
-
* check: `live` says whether a
|
|
29
|
-
*
|
|
30
|
-
* run`") while
|
|
32
|
+
* check: `live` says whether a team will consume it now, `when` is the " within ~2s" such a
|
|
33
|
+
* team picks it up in, and `tail` names the fallback ("takes effect on the next `tumwater
|
|
34
|
+
* run`") while tumwater is not running. Every marker command shares this so none can promise a
|
|
31
35
|
* timing the others deny — the one place that user-facing contract lives. */
|
|
32
36
|
export function markerApplyNote(root) {
|
|
33
37
|
const live = orchestratorAlive(root);
|
|
34
38
|
return {
|
|
35
39
|
live,
|
|
36
40
|
when: live ? " within ~2s" : "",
|
|
37
|
-
tail: live ? "" : ";
|
|
41
|
+
tail: live ? "" : "; tumwater is not running, so it takes effect on the next `tumwater start`",
|
|
38
42
|
};
|
|
39
43
|
}
|
|
40
|
-
/** The trailing liveness clause of a one-shot marker command's confirmation (reset-counters,
|
|
41
|
-
* and restart): with a live
|
|
42
|
-
*
|
|
43
|
-
* phrase their own confirmations around markerApplyNote's when/tail because their
|
|
44
|
-
* differ. */
|
|
44
|
+
/** The trailing liveness clause of a one-shot marker command's confirmation (reset-counters,
|
|
45
|
+
* wake, and restart): with a live team, "a running team <verb><when>"; without one, where the
|
|
46
|
+
* marker goes instead. The request* cores share this so their wording cannot drift — pause and
|
|
47
|
+
* resume phrase their own confirmations around markerApplyNote's when/tail because their
|
|
48
|
+
* sentences differ. */
|
|
45
49
|
function applyClause(live, when, verb) {
|
|
46
50
|
return live
|
|
47
|
-
? `
|
|
48
|
-
: "takes effect on the next `tumwater
|
|
51
|
+
? `the running team ${verb}${when}`
|
|
52
|
+
: "takes effect on the next `tumwater start` (tumwater is not running)";
|
|
49
53
|
}
|
|
50
54
|
/** The marker-writing core of `reset-counters`: zero each target's state file directly when no
|
|
51
|
-
*
|
|
55
|
+
* team is running, and drop the team marker a running team consumes within one poll cycle.
|
|
52
56
|
* Returns the confirmation the CLI prints verbatim. The CLI and the GUI's
|
|
53
57
|
* POST /api/reset-counters share this core. */
|
|
54
58
|
export function requestResetCounters(root, roles) {
|
|
55
|
-
// A live
|
|
59
|
+
// A live team's orchestrator is the state files' single writer: consumeResetRequest zeroes
|
|
56
60
|
// the affected runners' in-memory copies and the runners save them, and writes the file for a
|
|
57
61
|
// requested role that has no runner (a disabled role). Rewriting the files here too would be
|
|
58
62
|
// a second writer racing those whole-file saves — an overwrite from a snapshot the
|
|
59
63
|
// orchestrator has since moved past (a just-ended tick's result, a claim, a schedule), which
|
|
60
|
-
// the lost update discards. So with a live
|
|
64
|
+
// the lost update discards. So with a live team only the marker is written; with none there
|
|
61
65
|
// is no concurrent writer and the direct write is what carries the reset to the next run.
|
|
62
66
|
const { live, when } = markerApplyNote(root);
|
|
63
67
|
if (!live)
|
|
@@ -67,18 +71,25 @@ export function requestResetCounters(root, roles) {
|
|
|
67
71
|
// readJsonFile folds to null — the corrupt-marker superset that wakes/resets EVERY runner.
|
|
68
72
|
// For a scheduled wake (`--in`) that superset drops notBeforeMs, firing the wake hours early.
|
|
69
73
|
writeJsonAtomic(resetRequestPath(root), { at: Date.now(), roles });
|
|
70
|
-
// Only a live
|
|
71
|
-
// the next `tumwater
|
|
74
|
+
// Only a live team consumes the marker; without one the state files are already zeroed and
|
|
75
|
+
// the next `tumwater start` is when the in-memory copies catch up. Name which case this is
|
|
72
76
|
// rather than promising a ~2s pickup that no process will make.
|
|
73
77
|
return `counters reset for ${roles.join(", ")} — ${applyClause(live, when, "picks this up")}`;
|
|
74
78
|
}
|
|
79
|
+
/** The empty-list wording both wake-list renderers share — `cancelWakes`'s no-op arm and
|
|
80
|
+
* `renderWakeList`'s empty list: one literal so the two surfaces cannot drift on what an
|
|
81
|
+
* operator sees when nothing is pending, plus a ` for <role>` suffix when the view was
|
|
82
|
+
* role-scoped. */
|
|
83
|
+
function noWakePending(role) {
|
|
84
|
+
return `no scheduled wake pending${role ? ` for ${role}` : ""}`;
|
|
85
|
+
}
|
|
75
86
|
/** Drop pending scheduled wakes (`tumwater wake --cancel`, the dashboard's POST /api/wake-cancel):
|
|
76
87
|
* every pending wake, or only `role`'s share of them. Returns the confirmation the CLI prints
|
|
77
88
|
* verbatim and the GUI flashes, so the surfaces cannot drift on wording. */
|
|
78
89
|
export function cancelWakes(root, role) {
|
|
79
90
|
const n = cancelScheduledWakes(root, role);
|
|
80
91
|
const forRole = role ? ` for ${role}` : "";
|
|
81
|
-
return n > 0 ? `cancelled ${n
|
|
92
|
+
return n > 0 ? `cancelled ${plural(n, "scheduled wake")}${forRole}` : noWakePending(role);
|
|
82
93
|
}
|
|
83
94
|
/** The pending scheduled wakes (`tumwater wake --list`), earliest first; with `role`, only the
|
|
84
95
|
* wakes naming that loop. Entries keep their full roles list. Read-only. */
|
|
@@ -89,18 +100,18 @@ export function listWakes(root, role, now) {
|
|
|
89
100
|
/** `wake --list`'s text: one numbered line per wake, or the empty wording `cancelWakes` uses. */
|
|
90
101
|
export function renderWakeList(wakes, now, role) {
|
|
91
102
|
if (wakes.length === 0)
|
|
92
|
-
return
|
|
103
|
+
return noWakePending(role);
|
|
93
104
|
return wakes
|
|
94
|
-
.map((w, i) => `${i + 1}. in ${
|
|
105
|
+
.map((w, i) => `${i + 1}. in ${humanUntil(w.atMs, now)} — ${w.roles.join(", ")}`)
|
|
95
106
|
.join("\n");
|
|
96
107
|
}
|
|
97
108
|
/** The marker-writing core of `wake [--in <duration>]`, shared with the GUI's POST /api/wake:
|
|
98
109
|
* clear the named roles' backoff and pull nextRunAt to now (touching ONLY the schedule —
|
|
99
110
|
* counters, wake tracking, and session continuity are untouched), and drop the marker a
|
|
100
|
-
* running
|
|
111
|
+
* running team consumes within one poll. With a positive `inMs` (`wake --in`'s parsed
|
|
101
112
|
* duration) the wake is SCHEDULED: only the marker is written, its deadline this call's one
|
|
102
113
|
* clock read plus inMs — the state change belongs to the deadline, not to the submit, so a
|
|
103
|
-
*
|
|
114
|
+
* team stopped at submit (or restarted before the deadline) does not wake early;
|
|
104
115
|
* consumeWakeRequest clears the schedules when it consumes the marker at or after the
|
|
105
116
|
* deadline. The "wakes in" phrase is inMs itself, not the deadline minus a second clock read
|
|
106
117
|
* (the rule timedPauseBits follows): a caller-computed deadline let a millisecond tick between
|
|
@@ -111,10 +122,10 @@ export function requestWake(root, roles, inMs) {
|
|
|
111
122
|
// Deferred only for a genuinely positive delay: zero or less reads as immediate, so the
|
|
112
123
|
// immediate and scheduled paths share one consumer contract.
|
|
113
124
|
const deferred = inMs !== undefined && inMs > 0;
|
|
114
|
-
// Same single-writer rule as requestResetCounters: with a live
|
|
125
|
+
// Same single-writer rule as requestResetCounters: with a live team consumeWakeRequest
|
|
115
126
|
// clears the affected runners' in-memory schedules and the runners save them, and writes the
|
|
116
127
|
// file for a requested role that has no runner (a disabled role), so a direct state rewrite
|
|
117
|
-
// here would race those whole-file saves and lose one side's update. Without a
|
|
128
|
+
// here would race those whole-file saves and lose one side's update. Without a team there is
|
|
118
129
|
// no concurrent writer, and the file write is what carries the wake to the next run. A
|
|
119
130
|
// scheduled wake already skips the direct write — see the docstring.
|
|
120
131
|
const { live, when } = markerApplyNote(root);
|
|
@@ -130,27 +141,44 @@ export function requestWake(root, roles, inMs) {
|
|
|
130
141
|
}
|
|
131
142
|
else
|
|
132
143
|
writeJsonAtomic(wakeRequestPath(root), { at: now, roles });
|
|
133
|
-
// Same liveness contract as reset-counters and pause/resume: only a live
|
|
144
|
+
// Same liveness contract as reset-counters and pause/resume: only a live team consumes the
|
|
134
145
|
// marker, so say so instead of promising a poll that will not happen.
|
|
135
146
|
if (deferred)
|
|
136
147
|
return (`wake scheduled for ${roles.join(", ")} — wakes in ${durationLabel(inMs)} — ` +
|
|
137
148
|
(live
|
|
138
|
-
? "
|
|
139
|
-
: "
|
|
149
|
+
? "the running team applies it within one poll after the deadline"
|
|
150
|
+
: "tumwater is not running — the next `tumwater start` applies it after the deadline"));
|
|
140
151
|
return `wake requested for ${roles.join(", ")} — ${applyClause(live, when, "applies it")}`;
|
|
141
152
|
}
|
|
153
|
+
/** Schedule a graceful team stop `inMs` from now (`tumwater stop --in`, the dashboard's "Stop the
|
|
154
|
+
* team later"): the marker means nothing without a live team to consume it, so a stopped one
|
|
155
|
+
* refuses with NO_HARNESS_ERROR. The caller has already validated `inMs` (positive, within
|
|
156
|
+
* PAUSE_FOR_MAX_MS). */
|
|
157
|
+
export function requestScheduledStop(root, inMs) {
|
|
158
|
+
if (!orchestratorAlive(root))
|
|
159
|
+
return { ok: false, error: NO_HARNESS_ERROR };
|
|
160
|
+
scheduleStop(root, Date.now() + inMs);
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
message: `stop scheduled in ${durationLabel(inMs)} — the team finishes current work, then stops; \`tumwater stop --cancel\` drops it`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Drop a scheduled stop; needs no live team. The confirmation `stop --cancel` prints. */
|
|
167
|
+
export function cancelScheduledStopMessage(root) {
|
|
168
|
+
return cancelScheduledStop(root) ? "scheduled stop cancelled" : "no stop scheduled";
|
|
169
|
+
}
|
|
142
170
|
/** The marker-writing core of the forced restart, shared with the GUI's POST /api/restart:
|
|
143
|
-
* drop the marker a running
|
|
171
|
+
* drop the marker a running team consumes within one poll, telling the redeployer to waive
|
|
144
172
|
* its post-restart cooldown. Forcing never bypasses a refusal — a red main, a failed compile,
|
|
145
173
|
* a swap error, or a boot refusal still blocks — so a press is only meaningful while a restart
|
|
146
|
-
* is actually pending: with a live
|
|
147
|
-
* and the reply says so. With no
|
|
174
|
+
* is actually pending: with a live team whose published build is not stale, nothing is written
|
|
175
|
+
* and the reply says so. With no team running the marker is written harmlessly (the next `run`
|
|
148
176
|
* consumes it on its first poll) and the reply carries the same liveness contract as `wake`.
|
|
149
177
|
* Returns the structured confirmation the CLI prints verbatim and the GUI flashes. */
|
|
150
178
|
export function requestRestart(root) {
|
|
151
179
|
const info = readOrchestratorInfo(root);
|
|
152
180
|
if (info?.build && info.build.stale !== true) {
|
|
153
|
-
return { ok: false, error: "no
|
|
181
|
+
return { ok: false, error: "no update is waiting — tumwater is current with main" };
|
|
154
182
|
}
|
|
155
183
|
writeJsonAtomic(restartRequestPath(root), { at: Date.now() });
|
|
156
184
|
const { live, when } = markerApplyNote(root);
|
|
@@ -161,7 +189,7 @@ export function requestRestart(root) {
|
|
|
161
189
|
/** The marker-writing core of `tumwater reclaim` (plans/disk-floor.md, part 3/4): drop the
|
|
162
190
|
* marker a running orchestrator consumes within one poll and run one manual pass over every
|
|
163
191
|
* candidate, in-use worktrees respected. Returns the confirmation the CLI prints. A caller
|
|
164
|
-
* with no live
|
|
192
|
+
* with no live team must run the pass in-process instead (cmdReclaim), because nothing would
|
|
165
193
|
* consume the marker. */
|
|
166
194
|
function requestReclaim(root) {
|
|
167
195
|
writeJsonAtomic(reclaimRequestPath(root), { at: Date.now() });
|
|
@@ -171,9 +199,9 @@ function requestReclaim(root) {
|
|
|
171
199
|
/** Queue a prompt for one loop and wake that loop — the one workflow `tumwater prompt
|
|
172
200
|
* --role`, the dashboard's POST /api/prompt-role, and the TUI's Ctrl+R submit all share:
|
|
173
201
|
* submitRolePrompt enqueues into the loop's own queue (length-capped by the shared rule)
|
|
174
|
-
* and logs under the loop, then requestWake's single-role marker brings a live
|
|
202
|
+
* and logs under the loop, then requestWake's single-role marker brings a live team's
|
|
175
203
|
* loop in within one poll instead of whenever its backoff next expires (and is safe with
|
|
176
|
-
* no
|
|
204
|
+
* no team running — the same contract as `tumwater wake`). Returns requestWake's
|
|
177
205
|
* confirmation so a surface can show what the wake did. Keeping enqueue and wake in one
|
|
178
206
|
* call keeps a surface from ever queueing a prompt without the wake that delivers it
|
|
179
207
|
* promptly — the pairing is the invariant, not each surface's private discipline. An optional
|
|
@@ -185,7 +213,7 @@ export function submitRolePromptAndWake(root, role, text, images, notBeforeMs) {
|
|
|
185
213
|
return requestWake(root, [role]);
|
|
186
214
|
}
|
|
187
215
|
/** The marker-writing core of `abort`, shared with the GUI's POST /api/abort: drop the
|
|
188
|
-
* per-role marker a running
|
|
216
|
+
* per-role marker a running team consumes within one poll cycle. Reports the liveness gate
|
|
189
217
|
* and the director's discarded-prompt note structurally ({ok, message|error}) so the CLI can
|
|
190
218
|
* fail() and the GUI can shape its 409/200 — neither re-derives the wording. */
|
|
191
219
|
export function requestAbort(root, role) {
|
|
@@ -193,7 +221,7 @@ export function requestAbort(root, role) {
|
|
|
193
221
|
return { ok: false, error: NO_HARNESS_ERROR };
|
|
194
222
|
}
|
|
195
223
|
writeJsonAtomic(abortRequestPath(root, role), { at: Date.now() });
|
|
196
|
-
let confirmation = `abort requested for ${role} —
|
|
224
|
+
let confirmation = `abort requested for ${role} — the running team applies it within ~2s`;
|
|
197
225
|
if (role === DIRECTOR_ROLE) {
|
|
198
226
|
// The director's in-flight prompt was dequeued from the inbox file at tick start and an
|
|
199
227
|
// abort discards it without re-queueing — say so, since the discard is otherwise silent.
|
|
@@ -235,11 +263,11 @@ export function rolePauseMessage(root, role, changed, timed, now = Date.now()) {
|
|
|
235
263
|
return `role ${role} is already paused`;
|
|
236
264
|
const { when, tail } = markerApplyNote(root);
|
|
237
265
|
const { forPhrase, note } = timedPauseBits(timed, now);
|
|
238
|
-
return `role ${role} paused${forPhrase} — it
|
|
266
|
+
return `role ${role} paused${forPhrase} — it starts no new runs${when} (current runs finish; the rest of the team is unaffected)${note}${tail}`;
|
|
239
267
|
}
|
|
240
268
|
/** The per-role resume confirmation, shared between `tumwater resume --role` and the TUI's
|
|
241
|
-
* Ctrl+P for the same no-drift reason. Carries the
|
|
242
|
-
* phrases: the
|
|
269
|
+
* Ctrl+P for the same no-drift reason. Carries the team-pause interplay note cmdResume
|
|
270
|
+
* phrases: the team pause is the stronger gate, so with its marker present a freshly
|
|
243
271
|
* resumed role still starts no ticks (the director is exempt from that gate, so its
|
|
244
272
|
* resumption is real) — saying nothing would promise ticking the scheduler then deny, the
|
|
245
273
|
* same honest-confirmation contract markerApplyNote's tail serves. */
|
|
@@ -247,13 +275,13 @@ export function roleResumeMessage(root, role, changed) {
|
|
|
247
275
|
if (!changed)
|
|
248
276
|
return `role ${role} was not paused`;
|
|
249
277
|
const { when, tail } = markerApplyNote(root);
|
|
250
|
-
const
|
|
251
|
-
? " (the
|
|
278
|
+
const teamNote = isTeamPaused(root) && role !== DIRECTOR_ROLE
|
|
279
|
+
? " (the team pause is still active — `tumwater resume` lifts it)"
|
|
252
280
|
: "";
|
|
253
|
-
return `role ${role} resumed — it starts
|
|
281
|
+
return `role ${role} resumed — it starts new runs again${when}${teamNote}${tail}`;
|
|
254
282
|
}
|
|
255
283
|
/** The `tumwater pause --for <duration>` cap: 90 days. A timed pause is meant to answer "step
|
|
256
|
-
* away for a while and come back to a resumed
|
|
284
|
+
* away for a while and come back to a resumed team" — the same bound the report's day windows
|
|
257
285
|
* enforce (REPORT_MAX_DAYS in event-window.ts), and comfortably past any absence a deadline
|
|
258
286
|
* should encode. Beyond it the deadline stops being a timed pause and becomes a standing one:
|
|
259
287
|
* bare `pause` (lifted with `resume`) is that, and it says so instead of silently accepting a
|
|
@@ -262,7 +290,7 @@ export function roleResumeMessage(root, role, changed) {
|
|
|
262
290
|
* every surface shares, so no new `--for` consumer can outgrow it unnoticed. */
|
|
263
291
|
export const PAUSE_FOR_MAX_MS = 90 * 24 * 60 * 60 * 1000;
|
|
264
292
|
/** One manual reclaim, shared by `tumwater reclaim` and the dashboard's POST /api/reclaim: with
|
|
265
|
-
* a live
|
|
293
|
+
* a live team the pass is requested through the marker the orchestrator consumes (so in-use
|
|
266
294
|
* worktrees are respected); with none running it runs in-process, since nothing is in use.
|
|
267
295
|
* Returns the confirmation its caller prints verbatim and never fails, so a server can call it. */
|
|
268
296
|
export async function runManualReclaim(root) {
|
|
@@ -273,3 +301,13 @@ export async function runManualReclaim(root) {
|
|
|
273
301
|
return "nothing to reclaim";
|
|
274
302
|
return `reclaimed ${result.worktrees.join(", ")} — freed ${formatGB(result.freedGB)}`;
|
|
275
303
|
}
|
|
304
|
+
/** Wake the role named in an answered question's `(asked by <role> …)` clause, so it reads the
|
|
305
|
+
* decision it was blocked on without waiting out its backoff. Returns requestWake's confirmation,
|
|
306
|
+
* or null when the heading names no known role (asked by the user, an unknown or removed role,
|
|
307
|
+
* or no clause) — never throws for an unknown asker. */
|
|
308
|
+
export function wakeQuestionAsker(root, title) {
|
|
309
|
+
const asker = questionAsker(title);
|
|
310
|
+
if (asker === null || !knownRoleIdsCached(root).includes(asker))
|
|
311
|
+
return null;
|
|
312
|
+
return requestWake(root, [asker]);
|
|
313
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import { logEvent, warnEvent, warnEventBestEffort } from "../events/events.js";
|
|
4
|
+
import { logEvent, warnEvent, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
4
5
|
import { removeQuiet } from "../files/files.js";
|
|
5
6
|
import { errorMessage } from "../text/text.js";
|
|
6
7
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
7
8
|
import { finiteNumber } from "../files/json-object.js";
|
|
8
9
|
import { loadLoopState, saveLoopState, zeroCounters } from "../loop/loop-state.js";
|
|
10
|
+
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
9
11
|
import { clearBackoff } from "../scheduling/backoff.js";
|
|
10
12
|
import { abortRequestPath, reclaimRequestPath, resetRequestPath, restartRequestPath, scheduledWakeDir, statePath, wakeRequestPath, STATE_DIR, } from "../paths.js";
|
|
11
13
|
/** Read the optional `roles` request marker the `reset-counters` and `wake` CLI commands drop
|
|
@@ -28,13 +30,13 @@ function roleRequestTargets(markerFile, runners) {
|
|
|
28
30
|
function requestedRoles(markerFile) {
|
|
29
31
|
const marker = readJsonFile(markerFile);
|
|
30
32
|
return marker && Array.isArray(marker.roles) && marker.roles.every((r) => typeof r === "string")
|
|
31
|
-
? marker.roles
|
|
33
|
+
? marker.roles.map((r) => canonicalRoleId(r))
|
|
32
34
|
: null;
|
|
33
35
|
}
|
|
34
36
|
/** The requested roles that hold NO live runner and already have a persisted state file. A
|
|
35
37
|
* runner carries the operation to the role's in-memory copy and saves it; a role without one
|
|
36
38
|
* (disabled in tumwater.json, or outside a scoped round) is reached only through its state
|
|
37
|
-
* file, and while a
|
|
39
|
+
* file, and while a team is live this process is that file's single writer — the CLI wrote
|
|
38
40
|
* only the marker. A role with no state file has no counters or schedule to change, and a
|
|
39
41
|
* corrupt marker (no role list) yields none: the runner superset must not conjure new files
|
|
40
42
|
* for roles the marker never named. */
|
|
@@ -47,7 +49,7 @@ function runnerlessTargets(root, markerFile, runners) {
|
|
|
47
49
|
}
|
|
48
50
|
/** Best-effort write of one operator-request event. The request's side effects have already
|
|
49
51
|
* applied by the time this runs, so an unwritable events log must neither un-acknowledge the
|
|
50
|
-
* marker nor escape the orchestrator's poll body (whose loop has no catch) and end the
|
|
52
|
+
* marker nor escape the orchestrator's poll body (whose loop has no catch) and end the team.
|
|
51
53
|
* One wake consumer logs per role, so a throw that escaped would also starve the roles after
|
|
52
54
|
* the first. A failure is still reported on the same feed when that feed is writable. */
|
|
53
55
|
function logRequestEvent(root, event) {
|
|
@@ -56,7 +58,7 @@ function logRequestEvent(root, event) {
|
|
|
56
58
|
}
|
|
57
59
|
catch (err) {
|
|
58
60
|
try {
|
|
59
|
-
warnEvent(root,
|
|
61
|
+
warnEvent(root, TUMWATER_SOURCE, `could not record ${event.type} request: ${errorMessage(err)}`);
|
|
60
62
|
}
|
|
61
63
|
catch {
|
|
62
64
|
// The event feed is the failure; there is nothing left to report on.
|
|
@@ -65,15 +67,15 @@ function logRequestEvent(root, event) {
|
|
|
65
67
|
}
|
|
66
68
|
/** Report one role's failed state save for a consumed request, best-effort. These consumers run
|
|
67
69
|
* inside the orchestrator's catch-less poll loop, so an unwritable state file (ENOSPC, EACCES,
|
|
68
|
-
* the path replaced by a directory) must not throw out of the poll and end the
|
|
69
|
-
* disk pressure the
|
|
70
|
+
* the path replaced by a directory) must not throw out of the poll and end the team — the very
|
|
71
|
+
* disk pressure the team guards against would otherwise crash it. The caller has already applied
|
|
70
72
|
* the in-memory side effect where it can (a live runner), and a runnerless role's file keeps its
|
|
71
73
|
* previous content, so the warning is what names the un-persisted change. */
|
|
72
74
|
function warnStatePersistFailed(root, role, err) {
|
|
73
|
-
warnEventBestEffort(root,
|
|
75
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `could not persist ${role}'s state for your request: ${errorMessage(err)}`);
|
|
74
76
|
}
|
|
75
77
|
/** Consume a pending reset request from `tumwater reset-counters`, if any: the CLI zeroed the
|
|
76
|
-
* state files only when no
|
|
78
|
+
* state files only when no team was running; this zeroes the affected runners' in-memory
|
|
77
79
|
* copies (which then re-save), or their next tick's save would resurrect the pre-reset values,
|
|
78
80
|
* and writes the zeroed state file directly for requested roles with no runner, which the CLI
|
|
79
81
|
* left to its marker. */
|
|
@@ -109,27 +111,26 @@ export function consumeResetRequest(root, runners) {
|
|
|
109
111
|
const [only] = applied;
|
|
110
112
|
// One role → filed under that loop; several → one harness-level event listing them.
|
|
111
113
|
if (applied.length === 1 && only)
|
|
112
|
-
logRequestEvent(root, {
|
|
114
|
+
logRequestEvent(root, { source: only, type: "counters_reset" });
|
|
113
115
|
else
|
|
114
|
-
logRequestEvent(root, {
|
|
116
|
+
logRequestEvent(root, { source: TUMWATER_SOURCE, type: "counters_reset", roles: applied });
|
|
115
117
|
}
|
|
116
118
|
removeQuiet(markerFile);
|
|
117
119
|
}
|
|
118
|
-
/** Consume a pending wake request from `tumwater wake [--role <id>] [--in <duration>]`, if
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* deadline-crossing poll's `wake()` call is what applies the state change, since the submit
|
|
120
|
+
/** Consume a pending wake request from `tumwater wake [--role <id>] [--in <duration>]`, if any.
|
|
121
|
+
* Immediate wakes find the state files already cleared by the CLI when no team was running; this
|
|
122
|
+
* clears the affected runners' in-memory schedules (backoffSeconds, nextRunAt), or their next save
|
|
123
|
+
* would resurrect the pre-wake sleep window and the loops would keep sleeping until the original
|
|
124
|
+
* backoff expired, and writes the cleared schedule directly for requested roles with no runner,
|
|
125
|
+
* which the CLI left to its marker. A scheduled wake (`--in`) carries a `notBeforeMs`: before the
|
|
126
|
+
* deadline the marker is left in place (a later poll retries it — the wake takes effect within one
|
|
127
|
+
* poll cycle after the deadline, the same delivery granularity prompt --at's consumer gives), and
|
|
128
|
+
* the deadline-crossing poll's `wake()` call is what applies the state change, since the submit
|
|
128
129
|
* deliberately skipped it. A non-numeric or non-finite value (a hand edit's `1e999`) reads as
|
|
129
|
-
* immediate (defensive; validation happens at the CLI) — leaving Infinity as a deadline would
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
130
|
+
* immediate (defensive; validation happens at the CLI) — leaving Infinity as a deadline would hold
|
|
131
|
+
* `notBefore > Date.now()` true forever and strand the marker, which nothing else deletes. Each
|
|
132
|
+
* woken role logs the existing `wake` event with the operator reason, so the team's early ticks
|
|
133
|
+
* read in the feed as deliberate. */
|
|
133
134
|
export function consumeWakeRequest(root, runners) {
|
|
134
135
|
// A legacy wake.json (the pre-upgrade build wrote scheduled wakes here) keeps its deadline.
|
|
135
136
|
const markerFile = wakeRequestPath(root);
|
|
@@ -229,7 +230,7 @@ function applyWakeMarker(root, markerFile, runners) {
|
|
|
229
230
|
warnStatePersistFailed(root, r.role, err);
|
|
230
231
|
continue; // warn stands in for the wake event; nothing persisted to celebrate
|
|
231
232
|
}
|
|
232
|
-
logRequestEvent(root, {
|
|
233
|
+
logRequestEvent(root, { source: r.role, type: "wake", reason: "operator" }); // vocabulary-ok: wake reason code
|
|
233
234
|
}
|
|
234
235
|
for (const role of runnerlessTargets(root, markerFile, runners)) {
|
|
235
236
|
try {
|
|
@@ -239,7 +240,7 @@ function applyWakeMarker(root, markerFile, runners) {
|
|
|
239
240
|
warnStatePersistFailed(root, role, err);
|
|
240
241
|
continue;
|
|
241
242
|
}
|
|
242
|
-
logRequestEvent(root, {
|
|
243
|
+
logRequestEvent(root, { source: role, type: "wake", reason: "operator" }); // vocabulary-ok: wake reason code
|
|
243
244
|
}
|
|
244
245
|
removeQuiet(markerFile);
|
|
245
246
|
}
|
|
@@ -256,7 +257,7 @@ function consumeMarkerRequest(markerFile, act) {
|
|
|
256
257
|
/** Consume a pending restart request from the dashboard's build-stale alert (POST /api/restart),
|
|
257
258
|
* if any: tell the redeployer to force its next poll past the restart cooldown, then remove the
|
|
258
259
|
* marker. The redeployer logs the `restart_forced` event itself; with no redeployer (a
|
|
259
|
-
* non-self-hosting
|
|
260
|
+
* non-self-hosting team never has a stale build, so nothing could have written the marker) it
|
|
260
261
|
* is still cleaned up. */
|
|
261
262
|
export function consumeRestartRequest(root, redeployer) {
|
|
262
263
|
consumeMarkerRequest(restartRequestPath(root), () => redeployer?.forceRestart());
|
|
@@ -264,26 +265,25 @@ export function consumeRestartRequest(root, redeployer) {
|
|
|
264
265
|
/** Consume a pending manual reclaim request from `tumwater reclaim` (plans/disk-floor.md, part
|
|
265
266
|
* 3/4), if any: arm one background pass over every candidate on the controller — the same
|
|
266
267
|
* single-flight runner as pressure and idle reclaim — then remove the marker. With no
|
|
267
|
-
* controller (a
|
|
268
|
+
* controller (a team that predates the feature) the marker is still cleaned up. */
|
|
268
269
|
export function consumeReclaimRequest(root, reclaim) {
|
|
269
270
|
consumeMarkerRequest(reclaimRequestPath(root), () => reclaim?.requestManual());
|
|
270
271
|
}
|
|
271
|
-
/** Consume per-role abort requests from `tumwater abort --role <id>`: one marker file per
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
|
|
286
|
-
export function consumeAbortRequests(root, runners, landings) {
|
|
272
|
+
/** Consume per-role abort requests from `tumwater abort --role <id>`: one marker file per role (no
|
|
273
|
+
* parsing needed), so a request for an idle OR disabled loop is still cleaned up. A running tick
|
|
274
|
+
* gets killed and logs exactly one event; anything else is a silent no-op — the marker's presence
|
|
275
|
+
* IS the request, removing it acknowledges. Since merge queue 3/5 a role's in-flight work is often
|
|
276
|
+
* a MERGE rather than a tick, so the merge's controller is passed in: an abort for the role being
|
|
277
|
+
* merged right now kills that too (the drain's task sees `userAborted` and discards the pinned ref
|
|
278
|
+
* when the merge ends). There can be several in-flight units at once (land-queue speed 2c) — one
|
|
279
|
+
* per change being verified, the merge slot's stack, and each verified change waiting for it
|
|
280
|
+
* (flagged, then settled by the drain) — so `merges` lists them all and the request stops
|
|
281
|
+
* whichever holds the role. The merge is a whole STACK (merge queue 5/5), so the request matches
|
|
282
|
+
* ANY of its roles — `roles` is every role it is merging right now, and a stop for one of them
|
|
283
|
+
* kills the whole stack: the merger cannot split it (abandoning mid-stack would leave the pinned
|
|
284
|
+
* refs of the not-yet-processed changes for one-at-a-time recovery, which is already the
|
|
285
|
+
* fallback), and the merge discards every stacked ref when it ends. */
|
|
286
|
+
export function consumeAbortRequests(root, runners, merges) {
|
|
287
287
|
let markers;
|
|
288
288
|
try {
|
|
289
289
|
markers = fs.readdirSync(path.join(root, STATE_DIR));
|
|
@@ -295,7 +295,7 @@ export function consumeAbortRequests(root, runners, landings) {
|
|
|
295
295
|
// unwritable event feed is what failed, and there is nothing left to report on then.
|
|
296
296
|
const warnFailure = (role, err) => {
|
|
297
297
|
try {
|
|
298
|
-
warnEvent(root,
|
|
298
|
+
warnEvent(root, TUMWATER_SOURCE, `could not consume abort request for ${role}: ${errorMessage(err)}`);
|
|
299
299
|
}
|
|
300
300
|
catch {
|
|
301
301
|
// The feed is the failure; the remaining markers still get their turn below.
|
|
@@ -312,11 +312,11 @@ export function consumeAbortRequests(root, runners, landings) {
|
|
|
312
312
|
// after it forever. Apply the side effects first; a failure there keeps the marker for the
|
|
313
313
|
// next poll (retry), but still moves on so it cannot starve the others.
|
|
314
314
|
try {
|
|
315
|
-
for (const
|
|
316
|
-
if (!
|
|
315
|
+
for (const merge of merges) {
|
|
316
|
+
if (!merge.roles.includes(role))
|
|
317
317
|
continue;
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
merge.userAborted = true;
|
|
319
|
+
merge.controller.abort();
|
|
320
320
|
}
|
|
321
321
|
if (runner?.state.running)
|
|
322
322
|
runner.abortTick();
|
|
@@ -325,12 +325,12 @@ export function consumeAbortRequests(root, runners, landings) {
|
|
|
325
325
|
warnFailure(role, err);
|
|
326
326
|
continue;
|
|
327
327
|
}
|
|
328
|
-
// The side effects applied: acknowledge the request. The
|
|
328
|
+
// The side effects applied: acknowledge the request. The run_aborted event is
|
|
329
329
|
// observability, so a log failure must not un-acknowledge an abort that already happened.
|
|
330
330
|
removeQuiet(abortRequestPath(root, role));
|
|
331
331
|
if (runner?.state.running) {
|
|
332
332
|
try {
|
|
333
|
-
logEvent(root, {
|
|
333
|
+
logEvent(root, { source: role, type: "run_aborted" });
|
|
334
334
|
}
|
|
335
335
|
catch (err) {
|
|
336
336
|
warnFailure(role, err);
|