tumwater 0.4.2 → 0.4.3
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -9,15 +9,15 @@ import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_
|
|
|
9
9
|
import { owningTeamRoot } from "../paths.js";
|
|
10
10
|
import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
|
|
11
11
|
import { seedClaudeAccount } from "../agent/claude/claude-account.js";
|
|
12
|
-
import { seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
12
|
+
import { codexApiKeyRefusalMessage, seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
13
13
|
import { opencodeCatalogStale, readOpencodeCache, refreshOpencodeCatalog, } from "../agent/opencode/opencode-catalog.js";
|
|
14
14
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
15
|
import { HOUR_MS } from "../text/datetime.js";
|
|
16
16
|
import { errorMessage } from "../text/text.js";
|
|
17
17
|
import { backendById } from "../agent/agent-backend.js";
|
|
18
18
|
// Register the built-in pi and claude adapters into the registry this module reads (see
|
|
19
|
-
// unregisteredBackendInUse) — importing run
|
|
20
|
-
import "../agent/run
|
|
19
|
+
// unregisteredBackendInUse) — importing agent-run.ts is the one place they register.
|
|
20
|
+
import "../agent/agent-run.js";
|
|
21
21
|
/** The repo-level preconditions every repo-bound command shares (requireReadyRepo in
|
|
22
22
|
* cli/cli-repo-gate.ts): git
|
|
23
23
|
* on PATH, a repository, tumwater.json present, at least one commit. The first unmet one's
|
|
@@ -58,7 +58,7 @@ opencodeSeed = {}) {
|
|
|
58
58
|
// A backend an enabled seam routes to that this build has no registered adapter for cannot
|
|
59
59
|
// run its jobs: refuse to start rather than letting the run fall through to pi with a model
|
|
60
60
|
// id pi cannot serve (plans/opencode-backend.md part 2/7). Read the registry through
|
|
61
|
-
// run
|
|
61
|
+
// agent-run.ts, whose module scope registers the built-in pi and claude adapters.
|
|
62
62
|
const unregistered = unregisteredBackendInUse(config);
|
|
63
63
|
if (unregistered !== undefined) {
|
|
64
64
|
return { problem: `backend ${unregistered} is configured but this build has no adapter for it` };
|
|
@@ -88,7 +88,7 @@ opencodeSeed = {}) {
|
|
|
88
88
|
const codexAccount = seedOnce(root, config, "codex", codexSeedAttempted, () => seedCodexAccount(root, config));
|
|
89
89
|
if (codexAccount?.billing === "api") {
|
|
90
90
|
return {
|
|
91
|
-
problem: "
|
|
91
|
+
problem: codexApiKeyRefusalMessage("<home>"),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
// Seed the opencode models catalog (part 6b/7) before the first run: the poll surfaces price
|
|
@@ -189,7 +189,7 @@ export function opencodeProviders(config) {
|
|
|
189
189
|
return [...providers];
|
|
190
190
|
}
|
|
191
191
|
/** Every agent backend an enabled seam resolves to — the distinct agents of the one
|
|
192
|
-
* seamsInUse list (startup-
|
|
192
|
+
* seamsInUse list (startup-gates's consumer of it), so a claude or pi clause that exists only
|
|
193
193
|
* as a fallback, the resolver, or the director is checked exactly like a primary seam.
|
|
194
194
|
* One list the startup gate and doctor's binary check share, so a route the harness would
|
|
195
195
|
* spawn is checked on both surfaces. */
|
|
@@ -198,7 +198,7 @@ export function backendsInUse(config) {
|
|
|
198
198
|
}
|
|
199
199
|
/** The first backend an enabled seam routes to that this build has no registered adapter for,
|
|
200
200
|
* or undefined when every seam's backend is registered. The built-in pi and claude adapters
|
|
201
|
-
* are registered by importing src/agent/run
|
|
201
|
+
* are registered by importing src/agent/agent-run.ts (see the import above); a seam backend
|
|
202
202
|
* that is not in the registry is one this build cannot run. Doctor's binary check asks the
|
|
203
203
|
* same question, so the startup gate and doctor cannot disagree about which backend a build
|
|
204
204
|
* can serve. opencode's and codex's adapters now register like pi's and claude's, so only a
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose runs keep failing
|
|
2
2
|
* consecutively is warned at ERROR_STREAK_WARN (src/run/run-apply.ts) and named by the team-wide
|
|
3
|
-
* storm alarms (src/team/error-storm.ts, src/team/failure-spread.ts), but both
|
|
4
|
-
* role keeps running on the error ladder's 600 s max backoff forever,
|
|
5
|
-
* spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER
|
|
6
|
-
* pauses the role through the same per-role pause marker
|
|
3
|
+
* storm alarms (src/team/team-error-storm.ts, src/team/team-failure-spread.ts), but both
|
|
4
|
+
* only talk — the role keeps running on the error ladder's 600 s max backoff forever,
|
|
5
|
+
* burning a model slot and spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER
|
|
6
|
+
* consecutive failed runs this gate pauses the role through the same per-role pause marker
|
|
7
|
+
* the operator's `tumwater pause --role
|
|
7
8
|
* <id>` writes (src/team/team-state.ts's pauseRole — the marker, lock, and idempotence come
|
|
8
9
|
* free), so the scheduler's existing pausedRolesSet skip blocks new runs with zero scheduler
|
|
9
10
|
* changes. The director is NOT exempt: a failing director cannot process prompts anyway, and one
|
|
@@ -44,6 +45,73 @@ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
|
44
45
|
export function newStreakGateState() {
|
|
45
46
|
return { acked: new Map() };
|
|
46
47
|
}
|
|
48
|
+
/** Seed a fresh in-run ack cache from the persisted excused streak, so a restart keeps the
|
|
49
|
+
* grace the breaker (and a later operator resume) relies on instead of re-pausing a role
|
|
50
|
+
* whose frozen streak never ran down (BUGS.md 2026-10-23). */
|
|
51
|
+
function seedStreakAck(state, role, s) {
|
|
52
|
+
if (!state.acked.has(role) && s.streakAcked !== undefined) {
|
|
53
|
+
state.acked.set(role, s.streakAcked);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
|
|
57
|
+
* paused (no new runs start), so recording it here is exactly the level the resume excuses. */
|
|
58
|
+
function ackPausedStreak(root, role, streak, s, state) {
|
|
59
|
+
if ((state.acked.get(role) ?? 0) !== streak) {
|
|
60
|
+
state.acked.set(role, streak);
|
|
61
|
+
s.streakAcked = streak;
|
|
62
|
+
saveRunnerStateBestEffort(root, s);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A streak that reset through the role's own success ends the excused episode: the next one
|
|
66
|
+
* must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too. */
|
|
67
|
+
function clearResetStreak(root, role, s, state) {
|
|
68
|
+
if (state.acked.get(role) !== undefined) {
|
|
69
|
+
state.acked.delete(role);
|
|
70
|
+
delete s.streakAcked;
|
|
71
|
+
saveRunnerStateBestEffort(root, s);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Ack at trip time, before the pause is observed: the bar for the next trip is
|
|
75
|
+
* ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the role
|
|
76
|
+
* paused in between (an operator resume racing the next poll must not re-trip). The durable
|
|
77
|
+
* copy is written only once the pause is confirmed below — a failed or crashed pauseRole must
|
|
78
|
+
* not leave a permanent excused ack with no marker behind it. Returns the pause key when the
|
|
79
|
+
* pause was written, undefined when pauseRole failed. */
|
|
80
|
+
function tripStreakPause(root, role, streak, s, state, pauseKey) {
|
|
81
|
+
state.acked.set(role, streak);
|
|
82
|
+
if (!pauseRole(root, pauseKey(role)))
|
|
83
|
+
return undefined;
|
|
84
|
+
s.streakAcked = streak;
|
|
85
|
+
saveRunnerStateBestEffort(root, s);
|
|
86
|
+
logEventBestEffort(root, {
|
|
87
|
+
source: TUMWATER_SOURCE,
|
|
88
|
+
type: "role_streak_paused",
|
|
89
|
+
role: pauseKey(role),
|
|
90
|
+
streak,
|
|
91
|
+
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
92
|
+
});
|
|
93
|
+
return pauseKey(role);
|
|
94
|
+
}
|
|
95
|
+
/** One runner's turn in the poll: seed the in-run ack, ack while paused, clear on a reset,
|
|
96
|
+
* and trip past the bar. Returns the pause key when this poll tripped the role, undefined
|
|
97
|
+
* otherwise. The order is forced and unchanged: seed, then paused, then reset, then the bar,
|
|
98
|
+
* then trip. */
|
|
99
|
+
function pollStreakRunner(root, role, s, state, pausedRoles, pauseKey) {
|
|
100
|
+
const streak = s.consecutiveErrors ?? 0;
|
|
101
|
+
seedStreakAck(state, role, s);
|
|
102
|
+
if (runnerPaused(pausedRoles, role)) {
|
|
103
|
+
ackPausedStreak(root, role, streak, s, state);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (streak === 0) {
|
|
107
|
+
clearResetStreak(root, role, s, state);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const ack = state.acked.get(role) ?? 0;
|
|
111
|
+
if (streak < ERROR_STREAK_BREAKER + ack)
|
|
112
|
+
return undefined;
|
|
113
|
+
return tripStreakPause(root, role, streak, s, state, pauseKey);
|
|
114
|
+
}
|
|
47
115
|
/** Step the gate by one orchestrator poll. For each runner whose consecutive error streak
|
|
48
116
|
* has reached ERROR_STREAK_BREAKER plus what its ack already excused, and whose role is not
|
|
49
117
|
* in `pausedRoles`: write the per-role pause marker and log one `role_streak_paused` event
|
|
@@ -59,54 +127,9 @@ export function newStreakGateState() {
|
|
|
59
127
|
export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r) => r) {
|
|
60
128
|
const tripped = [];
|
|
61
129
|
for (const { role, state: s } of runners) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// whose frozen streak never ran down (BUGS.md 2026-10-23).
|
|
66
|
-
if (!state.acked.has(role) && s.streakAcked !== undefined) {
|
|
67
|
-
state.acked.set(role, s.streakAcked);
|
|
68
|
-
}
|
|
69
|
-
// Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
|
|
70
|
-
// paused (no new runs start), so recording it here is exactly the level the resume excuses.
|
|
71
|
-
if (runnerPaused(pausedRoles, role)) {
|
|
72
|
-
if ((state.acked.get(role) ?? 0) !== streak) {
|
|
73
|
-
state.acked.set(role, streak);
|
|
74
|
-
s.streakAcked = streak;
|
|
75
|
-
saveRunnerStateBestEffort(root, s);
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (streak === 0) {
|
|
80
|
-
// A streak that reset through the role's own success ends the excused episode: the
|
|
81
|
-
// next one must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too.
|
|
82
|
-
if (state.acked.get(role) !== undefined) {
|
|
83
|
-
state.acked.delete(role);
|
|
84
|
-
delete s.streakAcked;
|
|
85
|
-
saveRunnerStateBestEffort(root, s);
|
|
86
|
-
}
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const ack = state.acked.get(role) ?? 0;
|
|
90
|
-
if (streak < ERROR_STREAK_BREAKER + ack)
|
|
91
|
-
continue;
|
|
92
|
-
// Ack at trip time, before the pause is observed: the bar for the next trip is
|
|
93
|
-
// ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the
|
|
94
|
-
// role paused in between (an operator resume racing the next poll must not re-trip). The
|
|
95
|
-
// durable copy is written only once the pause is confirmed below — a failed or crashed
|
|
96
|
-
// pauseRole must not leave a permanent excused ack with no marker behind it.
|
|
97
|
-
state.acked.set(role, streak);
|
|
98
|
-
if (pauseRole(root, pauseKey(role))) {
|
|
99
|
-
s.streakAcked = streak;
|
|
100
|
-
saveRunnerStateBestEffort(root, s);
|
|
101
|
-
logEventBestEffort(root, {
|
|
102
|
-
source: TUMWATER_SOURCE,
|
|
103
|
-
type: "role_streak_paused",
|
|
104
|
-
role: pauseKey(role),
|
|
105
|
-
streak,
|
|
106
|
-
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
107
|
-
});
|
|
108
|
-
tripped.push(pauseKey(role));
|
|
109
|
-
}
|
|
130
|
+
const key = pollStreakRunner(root, role, s, state, pausedRoles, pauseKey);
|
|
131
|
+
if (key !== undefined)
|
|
132
|
+
tripped.push(key);
|
|
110
133
|
}
|
|
111
134
|
return tripped;
|
|
112
135
|
}
|
package/dist/src/git/git.js
CHANGED
|
@@ -64,40 +64,57 @@ const GIT_REDIRECT_ENV = ["GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_NAM
|
|
|
64
64
|
/** Refs git keeps per worktree (in the worktree's own git dir, not the common one). They are
|
|
65
65
|
* left to git. */
|
|
66
66
|
const PER_WORKTREE_REF = /^refs\/(?:bisect|worktree|rewritten)\//;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const gitdir = resolveGitDir(dir);
|
|
76
|
-
if (gitdir === undefined)
|
|
77
|
-
return undefined;
|
|
78
|
-
let commondir = gitdir;
|
|
67
|
+
/** Whether an environment variable makes git look somewhere other than the files `dir` holds. */
|
|
68
|
+
function gitRedirectsSet() {
|
|
69
|
+
return GIT_REDIRECT_ENV.some((name) => process.env[name]);
|
|
70
|
+
}
|
|
71
|
+
/** The shared refs/objects dir a gitdir points at via its `commondir` file: the gitdir itself
|
|
72
|
+
* when there is no file (a primary checkout), the resolved pointer when there is, and
|
|
73
|
+
* undefined when the file is blank or unreadable for any reason but absence. */
|
|
74
|
+
function resolveCommondir(gitdir) {
|
|
79
75
|
try {
|
|
80
76
|
const pointer = fs.readFileSync(path.join(gitdir, "commondir"), "utf8").trim();
|
|
81
77
|
if (pointer === "")
|
|
82
78
|
return undefined;
|
|
83
|
-
|
|
79
|
+
return path.resolve(gitdir, pointer);
|
|
84
80
|
}
|
|
85
81
|
catch (err) {
|
|
86
82
|
if (errCode(err) !== "ENOENT")
|
|
87
83
|
return undefined; // A primary checkout has none.
|
|
84
|
+
return gitdir;
|
|
88
85
|
}
|
|
86
|
+
}
|
|
87
|
+
/** Whether the files-backend trust checks hold: this process's user owns both dirs, the
|
|
88
|
+
* objects dir and HEAD are present, and no reftable store shadows them. */
|
|
89
|
+
function filesBackendTrusted(dir, gitdir, commondir, uid) {
|
|
89
90
|
try {
|
|
90
91
|
if (fs.statSync(dir).uid !== uid || fs.statSync(gitdir).uid !== uid)
|
|
91
|
-
return
|
|
92
|
+
return false;
|
|
92
93
|
if (!fs.statSync(path.join(commondir, "objects")).isDirectory())
|
|
93
|
-
return
|
|
94
|
+
return false;
|
|
94
95
|
if (!fs.statSync(path.join(gitdir, "HEAD")).isFile())
|
|
95
|
-
return
|
|
96
|
+
return false;
|
|
96
97
|
}
|
|
97
98
|
catch {
|
|
98
|
-
return
|
|
99
|
+
return false;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
return !fs.existsSync(path.join(commondir, "reftable"));
|
|
102
|
+
}
|
|
103
|
+
/** `dir`'s git dirs when its `.git` resolves from files to a files-backend repository this
|
|
104
|
+
* process's user owns, or undefined when git should decide. */
|
|
105
|
+
function repoFiles(dir) {
|
|
106
|
+
if (gitRedirectsSet())
|
|
107
|
+
return undefined;
|
|
108
|
+
const uid = process.getuid?.();
|
|
109
|
+
if (uid === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const gitdir = resolveGitDir(dir);
|
|
112
|
+
if (gitdir === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const commondir = resolveCommondir(gitdir);
|
|
115
|
+
if (commondir === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (!filesBackendTrusted(dir, gitdir, commondir, uid))
|
|
101
118
|
return undefined;
|
|
102
119
|
return { gitdir, commondir };
|
|
103
120
|
}
|
|
@@ -147,35 +164,33 @@ function storedRef(commondir, ref) {
|
|
|
147
164
|
}
|
|
148
165
|
return shaFromPackedRefs(packed, ` ${ref}`);
|
|
149
166
|
}
|
|
150
|
-
/** The
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (repo === undefined)
|
|
158
|
-
return undefined;
|
|
159
|
-
if (ref === "HEAD") {
|
|
160
|
-
let head;
|
|
161
|
-
try {
|
|
162
|
-
head = fs.readFileSync(path.join(repo.gitdir, "HEAD"), "utf8").trim();
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
if (isSha(head))
|
|
168
|
-
return head;
|
|
169
|
-
const target = head.startsWith("ref: ") ? head.slice("ref: ".length).trim() : "";
|
|
170
|
-
if (!target.startsWith("refs/heads/") || !isPlainRefName(target))
|
|
171
|
-
return undefined;
|
|
172
|
-
return storedRef(repo.commondir, target) ?? undefined;
|
|
167
|
+
/** The HEAD read: the detached SHA held in `<repo.gitdir>/HEAD`, else the SHA its `ref: `
|
|
168
|
+
* branch symref (a plain `refs/heads/…` name) resolves to, or undefined when the files do not
|
|
169
|
+
* settle it. */
|
|
170
|
+
function refShaFromHead(repo) {
|
|
171
|
+
let head;
|
|
172
|
+
try {
|
|
173
|
+
head = fs.readFileSync(path.join(repo.gitdir, "HEAD"), "utf8").trim();
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return undefined;
|
|
177
|
-
return storedRef(repo.commondir, ref) ?? undefined;
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
178
177
|
}
|
|
178
|
+
if (isSha(head))
|
|
179
|
+
return head;
|
|
180
|
+
const target = head.startsWith("ref: ") ? head.slice("ref: ".length).trim() : "";
|
|
181
|
+
if (!target.startsWith("refs/heads/") || !isPlainRefName(target))
|
|
182
|
+
return undefined;
|
|
183
|
+
return storedRef(repo.commondir, target) ?? undefined;
|
|
184
|
+
}
|
|
185
|
+
/** A full `refs/…` name: its plain-name and per-worktree guards, then the stored read. */
|
|
186
|
+
function refShaFromFullRefName(repo, ref) {
|
|
187
|
+
if (!isPlainRefName(ref) || PER_WORKTREE_REF.test(ref))
|
|
188
|
+
return undefined;
|
|
189
|
+
return storedRef(repo.commondir, ref) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
/** A short branch name: the DWIM-order guards (`<gitdir>/<name>`, `refs/<name>` and
|
|
192
|
+
* `refs/tags/<name>` all outrank the branch), then `refs/heads/<name>`. */
|
|
193
|
+
function refShaFromShortName(repo, ref) {
|
|
179
194
|
// A full object name means the object to git, ahead of any same-named ref.
|
|
180
195
|
if (isSha(ref) || !isPlainRefName(`refs/heads/${ref}`))
|
|
181
196
|
return undefined;
|
|
@@ -189,6 +204,21 @@ export function refShaFromFiles(dir, ref) {
|
|
|
189
204
|
}
|
|
190
205
|
return storedRef(repo.commondir, `refs/heads/${ref}`) ?? undefined;
|
|
191
206
|
}
|
|
207
|
+
/** The SHA `git rev-parse --verify <ref>` would print in `dir`, read from files, or undefined
|
|
208
|
+
* when the files do not settle it (see the section comment): the caller then spawns git. `ref`
|
|
209
|
+
* is HEAD (followed when it is a branch symref or a detached SHA), a full `refs/…` name, or a
|
|
210
|
+
* short branch name, taken only when git's DWIM order could not resolve it to anything else
|
|
211
|
+
* first (`<gitdir>/<name>`, `refs/<name>`, `refs/tags/<name>` all absent). */
|
|
212
|
+
export function refShaFromFiles(dir, ref) {
|
|
213
|
+
const repo = repoFiles(dir);
|
|
214
|
+
if (repo === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (ref === "HEAD")
|
|
217
|
+
return refShaFromHead(repo);
|
|
218
|
+
if (ref.startsWith("refs/"))
|
|
219
|
+
return refShaFromFullRefName(repo, ref);
|
|
220
|
+
return refShaFromShortName(repo, ref);
|
|
221
|
+
}
|
|
192
222
|
/** False when the files settle that `root`'s repository has no linked-worktree registrations
|
|
193
223
|
* (its `<commondir>/worktrees` admin dir is absent or empty), so `git worktree prune` has
|
|
194
224
|
* nothing to do. True when there are some, and undefined when git should decide. */
|
|
@@ -19,7 +19,6 @@ import { beginWorktreeUse } from "./worktree-use.js";
|
|
|
19
19
|
import { ensureDetachedWorktree, removeWorktreeDir } from "./worktree.js";
|
|
20
20
|
import { logEventBestEffort, warnEventBestEffort, tryOrWarn, TUMWATER_SOURCE } from "../events/events.js";
|
|
21
21
|
import { errorMessage } from "../text/text.js";
|
|
22
|
-
import { dropProviderLeaseKey } from "../concurrency/provider-lease.js";
|
|
23
22
|
/** In-process waiters per root, woken when any slot is released. Keyed by resolved root so two
|
|
24
23
|
* roots never wake each other. */
|
|
25
24
|
const waiters = new Map();
|
|
@@ -192,11 +191,9 @@ async function acquire(root, role, purpose, signal, slotBudget) {
|
|
|
192
191
|
logSlotWait(root, role, purpose, Date.now() - startedAt);
|
|
193
192
|
return chosen;
|
|
194
193
|
}
|
|
195
|
-
// The pool-slot wait
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// re-key waiter is waiting to drain; the next run re-takes the key as an ordinary re-key.
|
|
199
|
-
await dropProviderLeaseKey();
|
|
194
|
+
// The pool-slot wait needs no permit dropped: a run or verify holds only the one team
|
|
195
|
+
// permit (no provider key any more), so it parks on the busy pool like any other slot
|
|
196
|
+
// waiter.
|
|
200
197
|
const waiter = registerWaiter(root);
|
|
201
198
|
try {
|
|
202
199
|
await raceAbort(waiter.promise, signal);
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* The dashboard's GET settings/admin handlers (src/gui/gui-server.ts routes to them):
|
|
3
3
|
* prompt-text, doctor, config, retire-list, and brief. Each answers its request and touches no
|
|
4
4
|
* socket beyond its own `res`; the domain work lives one layer down (doctor/doctor.js,
|
|
5
|
-
* config/config.js, operator/retire.js), so this module only adapts HTTP onto it.
|
|
6
|
-
* into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
|
|
5
|
+
* config/config.js, operator/operator-retire.js), so this module only adapts HTTP onto it.
|
|
6
|
+
* it is barrelled into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
|
|
7
7
|
*/
|
|
8
8
|
import { runDoctor } from "../doctor/doctor.js";
|
|
9
9
|
import { knownRoleIds, loadConfigSafe } from "../config/config.js";
|
|
10
10
|
import { runnerEnabled } from "../roles/runner-ids.js";
|
|
11
|
-
import { artifactPhrase, previewRetire } from "../operator/retire.js";
|
|
11
|
+
import { artifactPhrase, previewRetire } from "../operator/operator-retire.js";
|
|
12
12
|
import { EDITABLE_CONFIG_KEYS } from "../config/config-editable-keys.js";
|
|
13
13
|
import { readBrief } from "../brief/brief.js";
|
|
14
14
|
import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* question-answer, backlog-file, backlog-cancel, backlog-release, backlog-top, and
|
|
5
5
|
* backlog-edit — the `checkedBacklogKind` / `checkedEntryPositionAndTitle` validators they share
|
|
6
6
|
* ride with them. Each handler adapts
|
|
7
|
-
* HTTP onto the domain work one layer down (backlog-write, question-answer, backlog-commit)
|
|
7
|
+
* HTTP onto the domain work one layer down (backlog-write, backlog-question-answer, backlog-commit)
|
|
8
8
|
* and touches no socket beyond its own `res`.
|
|
9
9
|
*/
|
|
10
10
|
import { backlogHolds, cancelledClaimNote, stillHeldNote } from "../backlog/backlog-eligibility.js";
|
|
@@ -15,7 +15,7 @@ import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
|
15
15
|
import { stampedSubject } from "../git/commit-message.js";
|
|
16
16
|
import { fileEntryWithBody, normalizeEntryBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry, bodyStructureProblem } from "../backlog/backlog-write.js";
|
|
17
17
|
import { tryMoveEntryToTop, tryReleaseEntry } from "../backlog/backlog-release.js";
|
|
18
|
-
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/question-answer.js";
|
|
18
|
+
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/backlog-question-answer.js";
|
|
19
19
|
import { promptLengthProblem } from "../inbox/inbox-submit.js";
|
|
20
20
|
import { requestWake, submitRolePromptAndWake, wakeQuestionAsker } from "../operator/operator-intent.js";
|
|
21
21
|
import { badRequest, okGone, sendJson } from "./gui-http-body.js";
|
|
@@ -202,8 +202,8 @@ async function respondBacklogWrite(res, root, kind, position, outcome, verb, sta
|
|
|
202
202
|
* `tumwater plan|bug --release <n>` through the non-exiting tryReleaseEntry. Body as
|
|
203
203
|
* backlog-cancel's. A released entry that is not still held is newly eligible, so the kind's
|
|
204
204
|
* role is woken (a held one wakes no role and carries `heldNote`); `gone` and `moved` answer
|
|
205
|
-
* okGone, and an entry with
|
|
206
|
-
*
|
|
205
|
+
* okGone, and an entry with no Refused or Needs review note answers status "not_refused" with no
|
|
206
|
+
* write and no wake. */
|
|
207
207
|
export async function handleBacklogRelease(req, res, root) {
|
|
208
208
|
const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
|
|
209
209
|
if (!body)
|
|
@@ -87,6 +87,62 @@ export function handleBacklogMatch(q, res, root) {
|
|
|
87
87
|
.map((row) => ({ role: row.role, file: row.file }));
|
|
88
88
|
sendJson(res, 200, out);
|
|
89
89
|
}
|
|
90
|
+
/** Normalise an optional `?path=` query arg to `{ path }` (absent → `{ path: undefined }`),
|
|
91
|
+
* answering the shared 400 when the raw value does not name a repo-relative path. Shared by
|
|
92
|
+
* the `?overlaps` and `?role` arms; `null` means the 400 was sent, so the caller returns. */
|
|
93
|
+
function diffPathParam(res, rawPath) {
|
|
94
|
+
if (rawPath === null)
|
|
95
|
+
return { path: undefined };
|
|
96
|
+
const path = normalizeDiffPath(rawPath);
|
|
97
|
+
if (path === null) {
|
|
98
|
+
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return { path };
|
|
102
|
+
}
|
|
103
|
+
/** The `?overlaps` view: refuse stat, normalise the path, resolve the comma-separated role
|
|
104
|
+
* list, collect the overlaps and scope them by roles then by path, then send. */
|
|
105
|
+
async function handleDiffOverlaps(q, res, root, rawPath, stat) {
|
|
106
|
+
if (stat) {
|
|
107
|
+
badRequest(res, "overlaps lists paths, not patches; drop stat");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const opath = diffPathParam(res, rawPath);
|
|
111
|
+
if (opath === null)
|
|
112
|
+
return;
|
|
113
|
+
const roles = roleListQuery(q, res, root);
|
|
114
|
+
if (roles === null)
|
|
115
|
+
return;
|
|
116
|
+
let view = await collectChangeOverlaps(root);
|
|
117
|
+
if (roles !== undefined)
|
|
118
|
+
view = scopeChangeOverlaps(view, roles);
|
|
119
|
+
if (opath.path !== undefined)
|
|
120
|
+
view = scopeChangeOverlapsToPath(view, opath.path);
|
|
121
|
+
sendJson(res, 200, view);
|
|
122
|
+
}
|
|
123
|
+
/** The role-less team roster: refuse stat and path with their own 400s, then send
|
|
124
|
+
* collectTeamChanges's document. */
|
|
125
|
+
async function handleDiffTeam(res, root, rawPath, stat) {
|
|
126
|
+
if (stat) {
|
|
127
|
+
badRequest(res, "stat needs role (the team roster carries no patch)");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (rawPath !== null) {
|
|
131
|
+
badRequest(res, "path needs role (the team roster carries no patch)");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
sendJson(res, 200, await collectTeamChanges(root));
|
|
135
|
+
}
|
|
136
|
+
/** The `?role=<id>` per-role view: validate through rejectBadRole, normalise the shared path,
|
|
137
|
+
* then send collectRoleChange with its path and stat options. */
|
|
138
|
+
async function handleDiffRole(res, root, role, rawPath, stat) {
|
|
139
|
+
if (rejectBadRole(root, res, role))
|
|
140
|
+
return;
|
|
141
|
+
const path = diffPathParam(res, rawPath);
|
|
142
|
+
if (path === null)
|
|
143
|
+
return;
|
|
144
|
+
sendJson(res, 200, await collectRoleChange(root, role, { ...(path.path === undefined ? {} : { path: path.path }), ...(stat ? { stat: true } : {}) }));
|
|
145
|
+
}
|
|
90
146
|
/** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
|
|
91
147
|
* collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
|
|
92
148
|
* `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
|
|
@@ -105,46 +161,10 @@ export function handleBacklogMatch(q, res, root) {
|
|
|
105
161
|
export async function handleDiff(q, res, root) {
|
|
106
162
|
const rawPath = q.get("path");
|
|
107
163
|
const stat = q.get("stat") !== null;
|
|
108
|
-
if (q.get("overlaps") !== null)
|
|
109
|
-
|
|
110
|
-
badRequest(res, "overlaps lists paths, not patches; drop stat");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const opath = rawPath === null ? undefined : normalizeDiffPath(rawPath);
|
|
114
|
-
if (opath === null) {
|
|
115
|
-
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const roles = roleListQuery(q, res, root);
|
|
119
|
-
if (roles === null)
|
|
120
|
-
return;
|
|
121
|
-
let view = await collectChangeOverlaps(root);
|
|
122
|
-
if (roles !== undefined)
|
|
123
|
-
view = scopeChangeOverlaps(view, roles);
|
|
124
|
-
if (opath !== undefined)
|
|
125
|
-
view = scopeChangeOverlapsToPath(view, opath);
|
|
126
|
-
sendJson(res, 200, view);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
164
|
+
if (q.get("overlaps") !== null)
|
|
165
|
+
return handleDiffOverlaps(q, res, root, rawPath, stat);
|
|
129
166
|
const role = q.get("role");
|
|
130
|
-
if (role === null)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (rawPath !== null) {
|
|
136
|
-
badRequest(res, "path needs role (the team roster carries no patch)");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
sendJson(res, 200, await collectTeamChanges(root));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (rejectBadRole(root, res, role))
|
|
143
|
-
return;
|
|
144
|
-
const path = rawPath === null ? undefined : normalizeDiffPath(rawPath);
|
|
145
|
-
if (path === null) {
|
|
146
|
-
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
sendJson(res, 200, await collectRoleChange(root, role, { ...(path === undefined ? {} : { path }), ...(stat ? { stat: true } : {}) }));
|
|
167
|
+
if (role === null)
|
|
168
|
+
return handleDiffTeam(res, root, rawPath, stat);
|
|
169
|
+
return handleDiffRole(res, root, role, rawPath, stat);
|
|
150
170
|
}
|
|
@@ -32,29 +32,19 @@ export function lanAddresses(interfaces = os.networkInterfaces()) {
|
|
|
32
32
|
* /proc/<pid>/cmdline exposes to every local user and what a reload re-runs verbatim — so the
|
|
33
33
|
* secret must travel by env, never argv. */
|
|
34
34
|
export const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
|
|
49
|
-
const allInterfaces = args.includes("--all-interfaces");
|
|
50
|
-
// A valueless or empty --token is a CLI error, not an open server: an operator who
|
|
51
|
-
// asked for protection must never silently get none. The validation and its wording
|
|
52
|
-
// live in tokenValueProblem, shared with `start --gui` so the two callers cannot
|
|
53
|
-
// drift; its flag-looking-value case names `tumwater gui` here.
|
|
54
|
-
const problem = tokenValueProblem(args, "tumwater gui");
|
|
55
|
-
if (problem)
|
|
56
|
-
fail(problem);
|
|
57
|
-
const flagToken = flagValue(args, "--token");
|
|
35
|
+
/** Resolve cmdGui's four injectable seams to their runnable forms: the server it starts, the
|
|
36
|
+
* LAN address source the banner names, the printer for the banner and warnings, and the
|
|
37
|
+
* serve-until-Ctrl+C wait. Production callers omit every seam and get the real server,
|
|
38
|
+
* interfaces, stdout, and wait. */
|
|
39
|
+
function resolveGuiSeams(seams) {
|
|
40
|
+
return {
|
|
41
|
+
start: seams.startGui ?? startGui,
|
|
42
|
+
lan: seams.lanAddresses ?? lanAddresses,
|
|
43
|
+
print: seams.say ?? say,
|
|
44
|
+
serve: seams.serve ?? (() => new Promise(() => { })),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function adoptGuiToken(flagToken, print) {
|
|
58
48
|
// A direct `tumwater gui --token` puts the secret on its own argv — readable by any local
|
|
59
49
|
// user and re-run verbatim by reexecSelf onto every reloaded generation — so fold it into
|
|
60
50
|
// the env and take it off the command line before a redeploy can re-expose it. A child
|
|
@@ -66,7 +56,10 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
66
56
|
process.argv.splice(argvToken, 2);
|
|
67
57
|
print(`--token is visible to every local user in this process's command line (\`ps\`, /proc/<pid>/cmdline) and is re-run onto each reloaded generation — prefer the ${GUI_TOKEN_ENV} env var`);
|
|
68
58
|
}
|
|
69
|
-
|
|
59
|
+
return flagToken ?? process.env[GUI_TOKEN_ENV] ?? "";
|
|
60
|
+
}
|
|
61
|
+
/** Start the server and translate the two common listen failures into hints naming their fix. */
|
|
62
|
+
async function startGuiOrExplain(start, root, port, allInterfaces, token) {
|
|
70
63
|
try {
|
|
71
64
|
await start(root, port, allInterfaces, token);
|
|
72
65
|
}
|
|
@@ -83,6 +76,9 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
83
76
|
throw new Error(`port ${port} could not be opened (permission denied) — ports below 1024 need root; pick an unprivileged port with \`tumwater gui --port <n>\``);
|
|
84
77
|
throw err;
|
|
85
78
|
}
|
|
79
|
+
}
|
|
80
|
+
/** Print the localhost banner and, under --all-interfaces, the LAN URLs and exposure wording. */
|
|
81
|
+
function printGuiBanner(print, lan, port, allInterfaces, token) {
|
|
86
82
|
const tokenSuffix = token ? `/?token=${encodeURIComponent(token)}` : "";
|
|
87
83
|
print(`tumwater gui at http://127.0.0.1:${port}${tokenSuffix} — Ctrl+C to stop`);
|
|
88
84
|
if (allInterfaces) {
|
|
@@ -95,5 +91,28 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
95
91
|
? "listening on ALL interfaces — token-protected; prompting the director requires the token"
|
|
96
92
|
: "listening on ALL interfaces — no auth; anyone reaching it can prompt the director");
|
|
97
93
|
}
|
|
94
|
+
}
|
|
95
|
+
/** The `tumwater gui` command: parse the --port/--all-interfaces/--token flags, start the
|
|
96
|
+
* server, print the banner, and serve until Ctrl+C. Lives beside startGui (gui/gui-server.ts) so
|
|
97
|
+
* the CLI's gui-specific policy (token validation, the listen-failure messages, the LAN
|
|
98
|
+
* announcement) cannot drift from the server it drives. Flag-vocabulary rejection stays in
|
|
99
|
+
* cli.ts with the other cases; everything gui-specific after that gate is this function's job.
|
|
100
|
+
* A failed listen throws (cli.ts's main catch renders it as the same `tumwater: …` line and
|
|
101
|
+
* exit 1 fail() would) so the listen-failure wordings stay assertable in-process. */
|
|
102
|
+
export async function cmdGui(root, args, seams = {}) {
|
|
103
|
+
const { start, lan, print, serve } = resolveGuiSeams(seams);
|
|
104
|
+
const portRaw = flagValue(args, "--port");
|
|
105
|
+
const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
|
|
106
|
+
const allInterfaces = args.includes("--all-interfaces");
|
|
107
|
+
// A valueless or empty --token is a CLI error, not an open server: an operator who
|
|
108
|
+
// asked for protection must never silently get none. The validation and its wording
|
|
109
|
+
// live in tokenValueProblem, shared with `start --gui` so the two callers cannot
|
|
110
|
+
// drift; its flag-looking-value case names `tumwater gui` here.
|
|
111
|
+
const problem = tokenValueProblem(args, "tumwater gui");
|
|
112
|
+
if (problem)
|
|
113
|
+
fail(problem);
|
|
114
|
+
const token = adoptGuiToken(flagValue(args, "--token") ?? null, print);
|
|
115
|
+
await startGuiOrExplain(start, root, port, allInterfaces, token);
|
|
116
|
+
printGuiBanner(print, lan, port, allInterfaces, token);
|
|
98
117
|
await serve(); // Serve until Ctrl+C.
|
|
99
118
|
}
|