tumwater 0.4.3 → 0.4.4
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 +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-run.js +2 -1
- package/dist/src/agent/claude/claude-account.js +6 -20
- package/dist/src/agent/codex/codex-account.js +5 -6
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-rollout.js +1 -1
- package/dist/src/agent/opencode/opencode-backend.js +24 -22
- package/dist/src/agent/opencode/opencode-catalog.js +35 -9
- package/dist/src/agent/session-ledger.js +22 -26
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-eligibility.js +14 -36
- package/dist/src/backlog/backlog-question-answer.js +1 -1
- package/dist/src/backlog/backlog.js +2 -1
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +0 -2
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-init.js +2 -2
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +4 -187
- package/dist/src/cli/completion.js +3 -3
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +2 -7
- package/dist/src/cli.js +19 -171
- package/dist/src/collections.js +11 -14
- package/dist/src/concurrency/lock.js +1 -1
- package/dist/src/config/config-keys.js +61 -20
- package/dist/src/config/config-request.js +11 -1
- package/dist/src/config/config-schema.js +1 -1
- package/dist/src/config/config-validation-roles.js +6 -22
- package/dist/src/config/config-validation.js +0 -1
- package/dist/src/config/config-views.js +17 -16
- package/dist/src/config/config-write.js +122 -79
- package/dist/src/config/config.js +1 -9
- package/dist/src/doctor/doctor-backlog-plans.js +29 -96
- package/dist/src/doctor/doctor-backlog.js +10 -10
- package/dist/src/doctor/doctor-checks.js +4 -4
- package/dist/src/doctor/doctor-claude-checks.js +27 -9
- package/dist/src/doctor/doctor-contention-checks.js +13 -5
- package/dist/src/doctor/doctor-fallback-checks.js +46 -24
- package/dist/src/doctor/doctor-holds.js +2 -2
- package/dist/src/doctor/doctor-orphans.js +3 -2
- package/dist/src/doctor/doctor-project-checks.js +21 -8
- package/dist/src/events/event-format.js +15 -11
- package/dist/src/events/event-legacy.js +2 -2
- package/dist/src/events/event-notify.js +4 -4
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +4 -13
- package/dist/src/failure/failure-data.js +4 -4
- package/dist/src/failure/failure-render.js +2 -2
- package/dist/src/failure/failure-state-change.js +50 -86
- package/dist/src/gates/gate-polls.js +5 -6
- package/dist/src/gates/startup-gates.js +2 -1
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-operator-commands.js +21 -25
- package/dist/src/gui/gui-prompt-commands.js +2 -2
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history.js +4 -4
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +6 -31
- package/dist/src/merge/merge-drain.js +3 -2
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +4 -3
- package/dist/src/operator/operator-commands.js +4 -3
- package/dist/src/orchestrator/orchestrator-poll.js +18 -38
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/scheduling-scope.js +17 -26
- package/dist/src/orchestrator/scheduling-skip.js +9 -13
- package/dist/src/paths.js +2 -2
- package/dist/src/pi/pi-event-line.js +27 -0
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-stream.js +9 -2
- package/dist/src/pi/pi-watchdogs.js +31 -10
- package/dist/src/process/process-group.js +2 -21
- package/dist/src/process/process.js +12 -4
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review.js +47 -40
- package/dist/src/review/review-precheck.js +3 -3
- package/dist/src/review/review.js +13 -22
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-render.js +6 -32
- package/dist/src/roles/role-view.js +2 -13
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +0 -9
- package/dist/src/run/run-stage.js +6 -6
- package/dist/src/run/run-verdict.js +13 -4
- package/dist/src/run/stage-check.js +17 -24
- package/dist/src/runner/runner-leftover.js +9 -26
- package/dist/src/runner/runner-outcome.js +1 -1
- package/dist/src/runner/runner-pi.js +6 -12
- package/dist/src/runner/runner-recovery.js +4 -12
- package/dist/src/runner/runner-revision.js +1 -2
- package/dist/src/runner/runner-run.js +7 -16
- package/dist/src/runner/runner-state-load.js +6 -19
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +34 -38
- package/dist/src/status/status-roles.js +1 -2
- package/dist/src/team/team-hold.js +7 -5
- package/dist/src/team/team-orchestrator-info.js +53 -21
- package/dist/src/team/team-polls.js +36 -86
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/phrases.js +9 -0
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +3 -2
- package/dist/src/ui/alert-feeds.js +0 -41
- package/dist/src/ui/badges.js +14 -75
- package/dist/src/ui/gui/gui-client-boot.js +0 -6
- package/dist/src/ui/gui/gui-client-drawer-config.js +1 -16
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-team.js +8 -2
- package/dist/src/ui/progress-data.js +54 -4
- package/dist/src/ui/run-progress-model.js +12 -3
- package/dist/src/ui/status-payload.js +10 -25
- package/dist/src/ui/status-render.js +10 -17
- package/dist/src/ui/team-alert-backlog.js +15 -205
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alerts.js +4 -10
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editors.js +5 -4
- package/dist/src/ui/tui/tui-input.js +8 -6
- package/dist/src/ui/tui/tui-pane-activity.js +2 -1
- package/dist/src/ui/tui/tui-pane-health.js +21 -23
- package/dist/src/ui/tui/tui-pane-history.js +7 -5
- package/dist/src/ui/tui/tui-pane-pending.js +19 -15
- package/dist/src/ui/tui/tui-pane-session.js +4 -3
- package/dist/src/ui/tui/tui-panes.js +9 -1
- package/dist/src/ui/tui/tui.js +3 -5
- package/dist/src/verdict/fix-claim.js +36 -7
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +2 -2
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/cli/{config-commands.js → cli-config-commands.js} +0 -0
- /package/dist/src/cli/{log-commands.js → cli-log-commands.js} +0 -0
- /package/dist/src/cli/{notify-command.js → cli-notify-command.js} +0 -0
- /package/dist/src/cli/{question-commands.js → cli-question-commands.js} +0 -0
|
@@ -24,44 +24,55 @@ import { deferredReasonText, untilClockSuffix, gigabytes, windowSuffix, windowRe
|
|
|
24
24
|
* selfcheck role's load-bearing rule asks it to
|
|
25
25
|
* judge — whether the harness's RESPONSE to a failure was wrong (plans/telemetry-role.md) —
|
|
26
26
|
* so they sit beside the outcomes rather than being dropped. */
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
27
|
+
const STATE_CHANGE_FAMILY = {
|
|
28
|
+
// Budget and window reopening.
|
|
29
|
+
budget_paused: budgetStateChange,
|
|
30
|
+
budget_fallback: budgetStateChange,
|
|
31
|
+
budget_resumed: budgetStateChange,
|
|
32
|
+
// Pauses and resumes of teams, roles and caps.
|
|
33
|
+
team_paused: pauseStateChange,
|
|
34
|
+
team_resumed: pauseStateChange,
|
|
35
|
+
role_paused: pauseStateChange,
|
|
36
|
+
role_resumed: pauseStateChange,
|
|
37
|
+
role_streak_paused: pauseStateChange,
|
|
38
|
+
role_cap_paused: pauseStateChange,
|
|
39
|
+
role_cap_resumed: pauseStateChange,
|
|
40
|
+
// Model and fallback wiring.
|
|
41
|
+
model_changed: modelStateChange,
|
|
42
|
+
fallback_switched: modelStateChange,
|
|
43
|
+
budget_handback: modelStateChange,
|
|
44
|
+
// Provider hold and resume.
|
|
45
|
+
rate_limit_hold: providerHoldStateChange,
|
|
46
|
+
rate_limit_resumed: providerHoldStateChange,
|
|
47
|
+
// Live-config edits.
|
|
48
|
+
max_concurrent_changed: liveConfigStateChange,
|
|
49
|
+
retention_changed: liveConfigStateChange,
|
|
50
|
+
config_changed: liveConfigStateChange,
|
|
51
|
+
// Self-hosted redeploys.
|
|
52
|
+
build_stale: updateStateChange,
|
|
53
|
+
restart_pending: updateStateChange,
|
|
54
|
+
restart: updateStateChange,
|
|
55
|
+
restart_refused: updateStateChange,
|
|
56
|
+
restart_blocked: updateStateChange,
|
|
57
|
+
// Need-based deferrals and gate holds/lifts.
|
|
58
|
+
run_deferred: waitStateChange,
|
|
59
|
+
disk_low: waitStateChange,
|
|
60
|
+
disk_ok: waitStateChange,
|
|
61
|
+
quiet_hours_started: waitStateChange,
|
|
62
|
+
quiet_hours_ended: waitStateChange,
|
|
63
|
+
maintenance_quota_hold: waitStateChange,
|
|
64
|
+
maintenance_quota_resumed: waitStateChange,
|
|
65
|
+
subscription_window_limited: waitStateChange,
|
|
66
|
+
subscription_window_reset: waitStateChange,
|
|
67
|
+
// Orchestrator lifecycle.
|
|
68
|
+
orchestrator_start: lifecycleStateChange,
|
|
69
|
+
orchestrator_stop: lifecycleStateChange,
|
|
70
|
+
supervisor_exit: lifecycleStateChange,
|
|
71
|
+
bootstrap_complete: lifecycleStateChange,
|
|
72
|
+
};
|
|
73
|
+
/** The event types the digest replays, derived from STATE_CHANGE_FAMILY's keys so the filter
|
|
74
|
+
* set can never drift from the dispatch. */
|
|
75
|
+
export const STATE_CHANGE_TYPES = new Set(Object.keys(STATE_CHANGE_FAMILY));
|
|
65
76
|
/** The event types the digest deliberately does not replay, one per EVENT_TYPE_LABELS key that
|
|
66
77
|
* is not a STATE_CHANGE_TYPES member. Together with STATE_CHANGE_TYPES it partitions
|
|
67
78
|
* EVENT_TYPE_LABELS' keys, so a new event type must be classified in one set or the other — the
|
|
@@ -416,52 +427,5 @@ function lifecycleStateChange(ev) {
|
|
|
416
427
|
* read as complete or stand mid-word where a key or value should be; the render adds the
|
|
417
428
|
* timestamp and a roleCell-sliced role, so no unbounded field reaches the page. */
|
|
418
429
|
export function describeStateChange(ev) {
|
|
419
|
-
|
|
420
|
-
case "budget_paused":
|
|
421
|
-
case "budget_fallback":
|
|
422
|
-
case "budget_resumed":
|
|
423
|
-
return truncateExample(budgetStateChange(ev), STATE_CHANGE_MAX);
|
|
424
|
-
case "team_paused":
|
|
425
|
-
case "team_resumed":
|
|
426
|
-
case "role_paused":
|
|
427
|
-
case "role_resumed":
|
|
428
|
-
case "role_streak_paused":
|
|
429
|
-
case "role_cap_paused":
|
|
430
|
-
case "role_cap_resumed":
|
|
431
|
-
return truncateExample(pauseStateChange(ev), STATE_CHANGE_MAX);
|
|
432
|
-
case "model_changed":
|
|
433
|
-
case "fallback_switched":
|
|
434
|
-
case "budget_handback":
|
|
435
|
-
return truncateExample(modelStateChange(ev), STATE_CHANGE_MAX);
|
|
436
|
-
case "rate_limit_hold":
|
|
437
|
-
case "rate_limit_resumed":
|
|
438
|
-
return truncateExample(providerHoldStateChange(ev), STATE_CHANGE_MAX);
|
|
439
|
-
case "max_concurrent_changed":
|
|
440
|
-
case "retention_changed":
|
|
441
|
-
case "config_changed":
|
|
442
|
-
return truncateExample(liveConfigStateChange(ev), STATE_CHANGE_MAX);
|
|
443
|
-
case "build_stale":
|
|
444
|
-
case "restart_pending":
|
|
445
|
-
case "restart":
|
|
446
|
-
case "restart_refused":
|
|
447
|
-
case "restart_blocked":
|
|
448
|
-
return truncateExample(updateStateChange(ev), STATE_CHANGE_MAX);
|
|
449
|
-
case "run_deferred":
|
|
450
|
-
case "disk_low":
|
|
451
|
-
case "disk_ok":
|
|
452
|
-
case "quiet_hours_started":
|
|
453
|
-
case "quiet_hours_ended":
|
|
454
|
-
case "maintenance_quota_hold":
|
|
455
|
-
case "maintenance_quota_resumed":
|
|
456
|
-
case "subscription_window_limited":
|
|
457
|
-
case "subscription_window_reset":
|
|
458
|
-
return truncateExample(waitStateChange(ev), STATE_CHANGE_MAX);
|
|
459
|
-
case "orchestrator_start":
|
|
460
|
-
case "orchestrator_stop":
|
|
461
|
-
case "supervisor_exit":
|
|
462
|
-
case "bootstrap_complete":
|
|
463
|
-
return truncateExample(lifecycleStateChange(ev), STATE_CHANGE_MAX);
|
|
464
|
-
default:
|
|
465
|
-
return truncateExample(ev.type, STATE_CHANGE_MAX);
|
|
466
|
-
}
|
|
430
|
+
return truncateExample(STATE_CHANGE_FAMILY[ev.type]?.(ev) ?? ev.type, STATE_CHANGE_MAX);
|
|
467
431
|
}
|
|
@@ -236,13 +236,12 @@ function pollTeamFailureAlarms(states, ctx) {
|
|
|
236
236
|
// Team-wide failure hold, per provider (src/team/team-hold.ts, PLANS.md 2026-10-05): once two
|
|
237
237
|
// roles' runs have ended on the SAME provider AND failure kind within a short window — 429s, or a
|
|
238
238
|
// connection, timeout, 5xx, or model-load backend failure — roles on THAT provider start no
|
|
239
|
-
// new runs
|
|
240
|
-
// spend a review strike on the storm — until the hold re-opens at its own deadline (Retry-After
|
|
239
|
+
// new runs until the hold re-opens at its own deadline (Retry-After
|
|
241
240
|
// honored on the rate-limit kind, doubling on a relapse, capped). Roles on a healthy provider
|
|
242
|
-
// keep running; a hold on the reviewer's provider
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
241
|
+
// keep running; a hold on the reviewer's provider parks only the no-role post-merge review
|
|
242
|
+
// drain — it holds no role start, verify or landing (no role waits on a reviewer), so the
|
|
243
|
+
// scheduling and start passes read no reviewer verdict from this map. The director's runs are
|
|
244
|
+
// exempt, as under the budget gate and the
|
|
246
245
|
// operator pause: an explicit human prompt outranks an autonomous gate, one director run is not
|
|
247
246
|
// the concurrency that sustains a storm, its 429 runs keep the per-run transient retry
|
|
248
247
|
// (backend-failure kinds ride this hold alone), and a prompt its run fails to fulfill goes back
|
|
@@ -10,6 +10,7 @@ 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
12
|
import { codexApiKeyRefusalMessage, seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
13
|
+
import { distinctOrdered } from "../collections.js";
|
|
13
14
|
import { opencodeCatalogStale, readOpencodeCache, refreshOpencodeCatalog, } from "../agent/opencode/opencode-catalog.js";
|
|
14
15
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
16
|
import { HOUR_MS } from "../text/datetime.js";
|
|
@@ -194,7 +195,7 @@ export function opencodeProviders(config) {
|
|
|
194
195
|
* One list the startup gate and doctor's binary check share, so a route the harness would
|
|
195
196
|
* spawn is checked on both surfaces. */
|
|
196
197
|
export function backendsInUse(config) {
|
|
197
|
-
return
|
|
198
|
+
return distinctOrdered(seamsInUse(config).map((s) => s.agent ?? "pi"));
|
|
198
199
|
}
|
|
199
200
|
/** The first backend an enabled seam routes to that this build has no registered adapter for,
|
|
200
201
|
* or undefined when every seam's backend is registered. The built-in pi and claude adapters
|
package/dist/src/git/git-diff.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
import { changeBaseRev, gitLines } from "./git.js";
|
|
6
6
|
import { git, gitTry } from "./git-run.js";
|
|
7
7
|
import { cutSplitsSurrogatePair } from "../text/text.js";
|
|
8
|
+
/** The one cap on a fetched change diff, shared by the repo-relative patch (aheadOfMainDiff,
|
|
9
|
+
* revisionInterdiff) and the view half's uncommitted patch (change-data.ts) so neither side
|
|
10
|
+
* of a reviewed change can print unbounded text and one bound is all they agree on. */
|
|
11
|
+
export const DIFF_MAX_BYTES = 200_000;
|
|
8
12
|
/** Git's C-quoting short escapes (its `sq_lookup` table) and the byte each decodes to. The
|
|
9
13
|
* mapping was a nine-case switch that repeated `bytes += …; break;` per entry; keeping it as
|
|
10
14
|
* data lets the decoder below handle the octal and literal fallbacks in one place. */
|
|
@@ -153,7 +157,7 @@ function splitDiffByFile(diff) {
|
|
|
153
157
|
* see, and a binary file's short "Binary files differ" stub naturally ranks below any real text
|
|
154
158
|
* change. A failed `git diff` throws: the reviewer handed an empty diff judged no change at
|
|
155
159
|
* all. A view catches it itself. */
|
|
156
|
-
export async function aheadOfMainDiff(wt, mainBranch, maxBytes =
|
|
160
|
+
export async function aheadOfMainDiff(wt, mainBranch, maxBytes = DIFF_MAX_BYTES, pathspec = []) {
|
|
157
161
|
const range = `${mainBranch}...HEAD`;
|
|
158
162
|
const full = await git(wt, "diff", range, ...pathspec);
|
|
159
163
|
if (full.length <= maxBytes)
|
|
@@ -181,7 +185,7 @@ export async function aheadOfMainDiff(wt, mainBranch, maxBytes = 200_000, pathsp
|
|
|
181
185
|
* the two versions even when the change is tiny: at the default 60% a two-line addition can
|
|
182
186
|
* fail to pair and render as two unrelated commits (`<`/`>`), which is exactly the case a
|
|
183
187
|
* small revision is. */
|
|
184
|
-
export async function revisionInterdiff(wt, mainBranch, priorSha, head, maxBytes =
|
|
188
|
+
export async function revisionInterdiff(wt, mainBranch, priorSha, head, maxBytes = DIFF_MAX_BYTES) {
|
|
185
189
|
try {
|
|
186
190
|
const priorBase = await changeBaseRev(wt, mainBranch, priorSha);
|
|
187
191
|
const headBase = await changeBaseRev(wt, mainBranch, head);
|
package/dist/src/gui/gui-args.js
CHANGED
|
@@ -80,6 +80,18 @@ export function rejectBadQueueFileName(res, name) {
|
|
|
80
80
|
badRequest(res, problem);
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
+
/** Pull a required `sha` commit id out of a POST body by the rule the History revert, the
|
|
84
|
+
* merge-queue cancel/top, and the post-review accept endpoints share: a value that is not a
|
|
85
|
+
* 4-64 hex-digit commit id (so never a flag or a ref name) is refused as a 400 naming the
|
|
86
|
+
* value. Returns the validated commit id, or null once the 400 is sent — the body-sha twin of
|
|
87
|
+
* requireStringField / requirePausedFlag, so the four operators' validation and 400 wording
|
|
88
|
+
* cannot drift. */
|
|
89
|
+
export function requireSha(res, sha) {
|
|
90
|
+
if (typeof sha === "string" && /^[0-9a-f]{4,64}$/i.test(sha))
|
|
91
|
+
return sha;
|
|
92
|
+
badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(sha)}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
83
95
|
/** Read an integer query parameter with the GUI's established discipline for an explicit
|
|
84
96
|
* count: absent → `fallback` (or, when no fallback is given, the parameter is required and
|
|
85
97
|
* its absence is its own 400 — "<name> required"); present but not a plain decimal integer
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The dashboard's POST config/budget command handlers (barrelled into
|
|
3
3
|
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts):
|
|
4
|
-
* config-set, config-unset, budget, role-cap, role-
|
|
4
|
+
* config-set, config-unset, budget, role-cap, role-enable,
|
|
5
5
|
* role-disable, and
|
|
6
6
|
* notify-test — the
|
|
7
7
|
* post-handler layer onto
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* own `res`.
|
|
10
10
|
*/
|
|
11
11
|
import { setConfigKey, setDailyBudgetUsd, setRoleModelFields, unsetConfigKey } from "../config/config-write.js";
|
|
12
|
-
import { loadConfigSafe } from "../config/config.js";
|
|
13
12
|
import { checkDailyBudgetUsd, checkMinRunInterval } from "../config/config-keys.js";
|
|
14
|
-
import { testNotifyHook } from "../cli/notify-command.js";
|
|
13
|
+
import { testNotifyHook } from "../cli/cli-notify-command.js";
|
|
15
14
|
import { rejectBadRole, requireConfigKey, requireRoleField, requireStringField } from "./gui-args.js";
|
|
16
15
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
17
16
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
@@ -88,67 +87,6 @@ export async function handleRoleCap(req, res, root) {
|
|
|
88
87
|
return configKeyError(res, "maxDailyCostUsdPerRole." + baseRole, result);
|
|
89
88
|
sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value, ...unchangedFlag(result) });
|
|
90
89
|
}
|
|
91
|
-
/** Handle POST /api/role-review-after-merge: the role drawer's Review row button moves one role
|
|
92
|
-
* in or out of review.afterMerge — the browser twin of `tumwater role <id>
|
|
93
|
-
* --review-after-merge`/`--clear-review-after-merge` (the CLI's just-landed write, the amber
|
|
94
|
-
* role-cap alert's remedy on the surface the alert appears on). Same body discipline as
|
|
95
|
-
* /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); the body carries the
|
|
96
|
-
* desired membership `afterMerge: true|false` (400 `afterMerge must be true or false`
|
|
97
|
-
* otherwise), and the role id maps to its base role's membership (baseRoleOf) exactly as the
|
|
98
|
-
* CLI's `--review-after-merge` branch does. An unknown id is refused up front (rejectBadRole
|
|
99
|
-
* on the base id, the mirror of the CLI's requireKnownRole) because a no-op or clear write
|
|
100
|
-
* skips validateConfig's known-role refusal — setConfigKey's section branch returns unchanged
|
|
101
|
-
* and writes nothing when the candidate equals the resolved list, so without it a bogus id
|
|
102
|
-
* would answer 200 on a clear. The whole-list value rides the same setConfigKey core `config
|
|
103
|
-
* set review.afterMerge <json>` uses, so validateConfig owns the string-array shape and the
|
|
104
|
-
* `review.afterMerge must not be "director"` refusal — no explicit director branch, matching
|
|
105
|
-
* the CLI. A no-op write (already in the asked-for state) answers unchanged and leaves
|
|
106
|
-
* tumwater.json byte-identical. */
|
|
107
|
-
export async function handleRoleReviewAfterMerge(req, res, root) {
|
|
108
|
-
const body = await readPostBody(req, res, '{"role": "feature", "afterMerge": true}');
|
|
109
|
-
if (!body)
|
|
110
|
-
return;
|
|
111
|
-
const role = requireRoleField(res, body);
|
|
112
|
-
if (role === null)
|
|
113
|
-
return;
|
|
114
|
-
const want = body.afterMerge;
|
|
115
|
-
if (typeof want !== "boolean") {
|
|
116
|
-
badRequest(res, "afterMerge must be true or false");
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const base = baseRoleOf(role);
|
|
120
|
-
// An unknown base id is refused up front — the mirror of the CLI's requireKnownRole. A
|
|
121
|
-
// no-op or clear write returns the list unchanged and setConfigKey skips validateConfig
|
|
122
|
-
// (the known-role check a real write gets for free), so without this guard a bogus id
|
|
123
|
-
// would answer 200 on the clear path (same reason handleRoleQuietHours's clear arm carries
|
|
124
|
-
// rejectBadRole). The stored list holds base ids, so the base id is screened — as
|
|
125
|
-
// validateConfig's checkKnownRoleId screens the written list — letting an instance id
|
|
126
|
-
// like feature-2 map to its known base and pass.
|
|
127
|
-
if (rejectBadRole(root, res, base))
|
|
128
|
-
return;
|
|
129
|
-
// The CLI's own computation, list-for-list: the whole value lands through the same
|
|
130
|
-
// setConfigKey core so validateConfig owns the string-array shape and the director
|
|
131
|
-
// refusal, and the write is atomic.
|
|
132
|
-
const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
|
|
133
|
-
const has = list.includes(base);
|
|
134
|
-
const next = has === want ? list : want ? [...list, base] : list.filter((r) => r !== base);
|
|
135
|
-
const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
|
|
136
|
-
if (!result.ok)
|
|
137
|
-
return configKeyError(res, "review.afterMerge", result);
|
|
138
|
-
sendJson(res, 200, {
|
|
139
|
-
ok: true,
|
|
140
|
-
role: base,
|
|
141
|
-
afterMerge: want,
|
|
142
|
-
message: result.unchanged
|
|
143
|
-
? want
|
|
144
|
-
? `${base}: already in review.afterMerge, so tumwater.json was not written`
|
|
145
|
-
: `${base}: not in review.afterMerge, so nothing was cleared`
|
|
146
|
-
: want
|
|
147
|
-
? `${base}: added to review.afterMerge`
|
|
148
|
-
: `${base}: removed from review.afterMerge`,
|
|
149
|
-
...unchangedFlag(result),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
90
|
/** Handle POST /api/role-interval: the role drawer's min run interval editor sets or clears
|
|
153
91
|
* one role's override of the team-wide minRunIntervalSeconds — the knob that makes one role
|
|
154
92
|
* act rarely, distinct from the team-wide value. Same body discipline as /api/role-cap
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
|
|
17
17
|
export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
|
|
18
|
-
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap,
|
|
18
|
+
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
|
|
19
19
|
export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
|
|
20
20
|
export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleAbort, handleAbortAll, handleStopSchedule, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleBriefEdit } from "./gui-operator-commands.js";
|
|
21
21
|
export { handleQuarantineRelease } from "./gui-quarantine-commands.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
|
|
8
8
|
* per-domain `checked*` validators live with their domain's module, not here.
|
|
9
9
|
*/
|
|
10
|
-
import { rejectBadRole } from "./gui-args.js";
|
|
10
|
+
import { rejectBadRole, requireSha } from "./gui-args.js";
|
|
11
11
|
import { readJsonObject, sendJson } from "./gui-http-body.js";
|
|
12
12
|
/** Every POST handler's shared opening: readJsonObject reads the body and — when it is
|
|
13
13
|
* oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
|
|
@@ -38,6 +38,24 @@ export async function readRoleBody(req, res, example, root, allowMissing = false
|
|
|
38
38
|
return null; // 400 already sent
|
|
39
39
|
return body;
|
|
40
40
|
}
|
|
41
|
+
/** The sha-addressed POST handlers' shared prologue: readPostBody (400/413 sent on a null
|
|
42
|
+
* return) plus requireSha's hex-commit-id validation (400 sent on a null return), so the
|
|
43
|
+
* queue-cancel/queue-top/post-review-accept/revert family states the body-discipline-and-sha
|
|
44
|
+
* check pair once instead of two guard lines per handler. Its null return means the response
|
|
45
|
+
* is already written and the handler must stop; callers keep their own `if (!read) return`
|
|
46
|
+
* bail. Exactly four call sites — handleRevert, handleQueueCancel, handleQueueTop, and
|
|
47
|
+
* handlePostReviewAccept — returning the body too so handleRevert can read its optional
|
|
48
|
+
* dryRun; the other three callers use only the sha. A caller that needs neither the body nor
|
|
49
|
+
* the example can read just `{ sha }` from the result. */
|
|
50
|
+
export async function readShaBody(req, res) {
|
|
51
|
+
const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
|
|
52
|
+
if (body === null)
|
|
53
|
+
return null; // 4xx already sent — oversized, malformed, or not an object
|
|
54
|
+
const sha = requireSha(res, body.sha);
|
|
55
|
+
if (sha === null)
|
|
56
|
+
return null; // 400 already sent
|
|
57
|
+
return { body, sha };
|
|
58
|
+
}
|
|
41
59
|
/** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
|
|
42
60
|
* message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
|
|
43
61
|
* reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
|
|
@@ -32,7 +32,7 @@ import { editBriefText } from "../brief/brief-edit.js";
|
|
|
32
32
|
import { requireStringField, validRoleIds } from "./gui-args.js";
|
|
33
33
|
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
34
34
|
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
35
|
-
import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
35
|
+
import { readPostBody, readRoleBody, readShaBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
36
36
|
import { wakeInSecondsMs } from "./gui-team-commands.js";
|
|
37
37
|
/** The shared body of the dashboard's Health/Action buttons that run a domain function and
|
|
38
38
|
* answer its text lines joined with "; ": readPostBody (400/413 sent on a null return) plus the
|
|
@@ -249,20 +249,19 @@ export async function handleRetire(req, res, root) {
|
|
|
249
249
|
* answers `{ ok, preview }` (previewRevert, writing nothing); otherwise the revert commits and
|
|
250
250
|
* answers `{ ok, headSha, beforeSha, message }`. A core's refusal is a 409 with its wording. */
|
|
251
251
|
export async function handleRevert(req, res, root) {
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
252
|
+
const parsed = await readShaBody(req, res);
|
|
253
|
+
if (parsed === null)
|
|
254
254
|
return;
|
|
255
|
-
|
|
256
|
-
return badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(body.sha)}`);
|
|
255
|
+
const { body, sha } = parsed;
|
|
257
256
|
if (body.dryRun !== undefined && typeof body.dryRun !== "boolean")
|
|
258
257
|
return badRequest(res, `"dryRun" must be true or false${gotSuffix(body.dryRun)}`);
|
|
259
258
|
if (body.dryRun === true) {
|
|
260
|
-
const preview = await previewRevert(root,
|
|
259
|
+
const preview = await previewRevert(root, sha);
|
|
261
260
|
if (!preview.ok)
|
|
262
261
|
return sendJson(res, 409, { error: preview.error });
|
|
263
262
|
return sendJson(res, 200, { ok: true, preview });
|
|
264
263
|
}
|
|
265
|
-
const result = await revertMergedCommit(root,
|
|
264
|
+
const result = await revertMergedCommit(root, sha);
|
|
266
265
|
if (!result.ok)
|
|
267
266
|
return sendJson(res, 409, { error: result.error });
|
|
268
267
|
const followUp = revertFollowUp(result.subject);
|
|
@@ -292,14 +291,13 @@ function rejectInFlightMerge(res, root, sha) {
|
|
|
292
291
|
* raced the click) is a 200 `{ status: "gone" }` data, not an error. Otherwise it answers the
|
|
293
292
|
* cancelled entry's role/commit/summary and removes the queue file and pin. */
|
|
294
293
|
export async function handleQueueCancel(req, res, root) {
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
294
|
+
const parsed = await readShaBody(req, res);
|
|
295
|
+
if (parsed === null)
|
|
297
296
|
return;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (rejectInFlightMerge(res, root, body.sha))
|
|
297
|
+
const { sha } = parsed;
|
|
298
|
+
if (rejectInFlightMerge(res, root, sha))
|
|
301
299
|
return;
|
|
302
|
-
const outcome = await cancelQueuedMergeBySha(root,
|
|
300
|
+
const outcome = await cancelQueuedMergeBySha(root, sha);
|
|
303
301
|
if (outcome.status === "gone")
|
|
304
302
|
return sendJson(res, 200, { ok: true, status: "gone" });
|
|
305
303
|
if (outcome.status !== "cancelled")
|
|
@@ -322,12 +320,11 @@ export async function handleQueueCancel(req, res, root) {
|
|
|
322
320
|
* accepted like any pending one. Otherwise it answers the
|
|
323
321
|
* accepted entry's role/commit/summary and removes the entry. */
|
|
324
322
|
export async function handlePostReviewAccept(req, res, root) {
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
323
|
+
const parsed = await readShaBody(req, res);
|
|
324
|
+
if (parsed === null)
|
|
327
325
|
return;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const outcome = acceptPostReviewBySha(root, body.sha);
|
|
326
|
+
const { sha } = parsed;
|
|
327
|
+
const outcome = acceptPostReviewBySha(root, sha);
|
|
331
328
|
if (outcome.status === "gone")
|
|
332
329
|
return sendJson(res, 200, { ok: true, status: "gone" });
|
|
333
330
|
if (outcome.status === "settled")
|
|
@@ -352,18 +349,17 @@ export async function handlePostReviewAccept(req, res, root) {
|
|
|
352
349
|
* Otherwise it answers the topped entry's role/commit/summary and moves its queue file to
|
|
353
350
|
* the front. */
|
|
354
351
|
export async function handleQueueTop(req, res, root) {
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
352
|
+
const parsed = await readShaBody(req, res);
|
|
353
|
+
if (parsed === null)
|
|
357
354
|
return;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (rejectInFlightMerge(res, root, body.sha))
|
|
355
|
+
const { sha } = parsed;
|
|
356
|
+
if (rejectInFlightMerge(res, root, sha))
|
|
361
357
|
return;
|
|
362
|
-
const outcome = moveQueuedMergeToTopBySha(root,
|
|
358
|
+
const outcome = moveQueuedMergeToTopBySha(root, sha);
|
|
363
359
|
if (outcome.status === "gone")
|
|
364
360
|
return sendJson(res, 200, { ok: true, status: "gone" });
|
|
365
361
|
if (outcome.status === "already-first")
|
|
366
|
-
return sendJson(res, 200, { ok: true, status: "already-first", role: outcome.role, commit: outcome.sha, summary: outcome.summary, message: `${shortSha(
|
|
362
|
+
return sendJson(res, 200, { ok: true, status: "already-first", role: outcome.role, commit: outcome.sha, summary: outcome.summary, message: `${shortSha(sha)} is already at the front` });
|
|
367
363
|
if (outcome.status !== "topped")
|
|
368
364
|
return;
|
|
369
365
|
sendJson(res, 200, {
|
|
@@ -13,7 +13,7 @@ import { promptPreview } from "../inbox/inbox.js";
|
|
|
13
13
|
import { cancelQueuedFile } from "../inbox/inbox-cancel.js";
|
|
14
14
|
import { editQueuedFile } from "../inbox/inbox-edit.js";
|
|
15
15
|
import { topQueuedFile } from "../inbox/inbox-top.js";
|
|
16
|
-
import { parseDurationMs } from "../cli/cli-args.js";
|
|
16
|
+
import { durationLabel, parseDurationMs } from "../cli/cli-args.js";
|
|
17
17
|
import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
18
18
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
19
19
|
import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
|
|
@@ -70,7 +70,7 @@ function checkedPromptDefer(res, body) {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
if (ms > PAUSE_FOR_MAX_MS) {
|
|
73
|
-
badRequest(res, `defer is capped at
|
|
73
|
+
badRequest(res, `defer is capped at ${durationLabel(PAUSE_FOR_MAX_MS)}${gotSuffix(body.defer)}`);
|
|
74
74
|
return null;
|
|
75
75
|
}
|
|
76
76
|
return Date.now() + ms;
|
|
@@ -13,7 +13,7 @@ import { captureStartupBuild, createReloadWatch, reexecSelf, watchReloadSupervis
|
|
|
13
13
|
import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
|
|
14
14
|
import { errorMessage } from "../text/text.js";
|
|
15
15
|
import { handleBacklog, handleBacklogMatch, handleBrief, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleRun, handleRole, handleTranscript, handleDoctor, handleRetireList, } from "./gui-endpoints.js";
|
|
16
|
-
import { handleAbort, handleAbortAll, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap,
|
|
16
|
+
import { handleAbort, handleAbortAll, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handlePromptTop, handleQuestionAnswer, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleRestart, handleStopSchedule, handleResetCounters, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleQuarantineRelease, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
|
|
17
17
|
import { sendJson } from "./gui-http-body.js";
|
|
18
18
|
/** Constant-time credential comparison for the shared-token gate: `timingSafeEqual` throws
|
|
19
19
|
* on unequal lengths, so the length equality is the guard. A naive `===` string compare
|
|
@@ -176,7 +176,6 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
|
|
|
176
176
|
"POST /api/question-answer": (req, res) => handleQuestionAnswer(req, res, root),
|
|
177
177
|
"POST /api/budget": (req, res) => handleBudget(req, res, root),
|
|
178
178
|
"POST /api/role-cap": (req, res) => handleRoleCap(req, res, root),
|
|
179
|
-
"POST /api/role-review-after-merge": (req, res) => handleRoleReviewAfterMerge(req, res, root),
|
|
180
179
|
"POST /api/role-enable": (req, res) => handleRoleEnable(req, res, root),
|
|
181
180
|
"POST /api/role-disable": (req, res) => handleRoleDisable(req, res, root),
|
|
182
181
|
"POST /api/role-interval": (req, res) => handleRoleInterval(req, res, root),
|
|
@@ -11,6 +11,7 @@ import { cancelWakes, PAUSE_FOR_MAX_MS, requestWake } from "../operator/operator
|
|
|
11
11
|
import { requirePausedFlag, validRoleIds } from "./gui-args.js";
|
|
12
12
|
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
13
13
|
import { gotSuffix } from "../text/text.js";
|
|
14
|
+
import { durationLabel } from "../cli/cli-args.js";
|
|
14
15
|
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
15
16
|
/** The shared optional-seconds rule behind the timed pause's `forSeconds` and the scheduled
|
|
16
17
|
* wake/stop's `inSeconds`: the field is absent (undefined) or a positive finite number of
|
|
@@ -30,7 +31,7 @@ function boundedSecondsMs(res, body, field, capHint = "") {
|
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
33
|
if (seconds * 1000 > PAUSE_FOR_MAX_MS) {
|
|
33
|
-
badRequest(res, `${field} is capped at ${PAUSE_FOR_MAX_MS
|
|
34
|
+
badRequest(res, `${field} is capped at ${durationLabel(PAUSE_FOR_MAX_MS)}${gotSuffix(seconds)}${capHint}`);
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
36
37
|
return Math.round(seconds * 1000);
|
|
@@ -3,7 +3,7 @@ import { betweenWindow, durationLabel, failRivalShapes, failUntilShorterThanSinc
|
|
|
3
3
|
import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, HISTORY_SUMMARY_GROUPS, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, runRows, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels } from "./history-data.js";
|
|
4
4
|
import { renderModelSummary, renderRow, renderSummary, widenWidths, widestWidths } from "./history-render.js";
|
|
5
5
|
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
6
|
-
import { followUntilParentGone } from "../cli/log-commands.js";
|
|
6
|
+
import { followUntilParentGone } from "../cli/cli-log-commands.js";
|
|
7
7
|
import { hasFollowFlag } from "../cli/cli-flag-specs.js";
|
|
8
8
|
import { parseEventLine, readEventsTailWithEnd } from "../events/event-read.js";
|
|
9
9
|
import { eventsLogPath } from "../paths.js";
|
|
@@ -23,7 +23,7 @@ export function parseHistoryModelList(raw) {
|
|
|
23
23
|
return pieces;
|
|
24
24
|
}
|
|
25
25
|
/** `tumwater history [--role <id>[,<id>...]] [-n N]`: one row per completed run, newest first. The
|
|
26
|
-
* observing half beside cmdLogs (log-commands.ts): read-only over the event log, stdout only —
|
|
26
|
+
* observing half beside cmdLogs (cli-log-commands.ts): read-only over the event log, stdout only —
|
|
27
27
|
* every datum rides the `run_end` events src/run/run-finalize.ts already writes, so this
|
|
28
28
|
* is a rendering of the existing record, not a new one. The rows themselves are collected
|
|
29
29
|
* by history-data.ts (beside src/report/report-data.ts and src/failure/failure-data.ts), and
|
|
@@ -31,7 +31,7 @@ export function parseHistoryModelList(raw) {
|
|
|
31
31
|
* src/report/report-render.ts); this module parses the args, drives the CLI, and prints. */
|
|
32
32
|
/** The missing-pattern error cmdHistory prints for a valueless or empty `--grep`, exported so
|
|
33
33
|
* cli.ts's rejectUnknownArgs spec for --grep can fail a trailing `history --grep` with the same
|
|
34
|
-
* wording. Kept local to this module — log-commands.ts holds its own GREP_VALUE_ERROR — so the
|
|
34
|
+
* wording. Kept local to this module — cli-log-commands.ts holds its own GREP_VALUE_ERROR — so the
|
|
35
35
|
* two observing views' wordings each name the command that printed them. */
|
|
36
36
|
export const HISTORY_GREP_VALUE_ERROR = "history --grep needs a pattern";
|
|
37
37
|
/** The missing-value error cmdHistory prints for a valueless or empty `--result`, exported so
|
|
@@ -192,7 +192,7 @@ function sayHistoryEmpty(q, filtered) {
|
|
|
192
192
|
// `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
|
|
193
193
|
// rows were scanned and filtered out — the operator would read a quiet team where the
|
|
194
194
|
// truth is `the runs are there, none match`. The logs --grep rule (its `no events
|
|
195
|
-
// matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
|
|
195
|
+
// matching "<pattern>"`, src/cli/cli-log-commands.ts) names the pattern instead; --json stays
|
|
196
196
|
// a bare {"rows":[]} document above, never prose in either form.
|
|
197
197
|
//
|
|
198
198
|
// The result class is named only when it emptied a non-empty set (`afterGrep > 0`): an
|
|
@@ -3,7 +3,7 @@ import { mergeRefName } from "../paths.js";
|
|
|
3
3
|
import { recordReview } from "../run/run-apply.js";
|
|
4
4
|
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
5
5
|
import { unverifiedTreeOutcome } from "../build/build-check-events.js";
|
|
6
|
-
import { checkFailureReasons,
|
|
6
|
+
import { checkFailureReasons, redCheckHeadline } from "../build/build-check-report.js";
|
|
7
7
|
import { onlyKnownMainFailures } from "../baseline/main-watch.js";
|
|
8
8
|
import { mainTipVerdict } from "../baseline/main-red.js";
|
|
9
9
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
@@ -126,7 +126,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
|
|
|
126
126
|
if (result === "merge_blocked" && !attributed) {
|
|
127
127
|
warnEventBestEffort(ctx.root, role, `merge check failed (kept for one retry): ${reason}`);
|
|
128
128
|
if (ctx.state.mergeCheckFailures) {
|
|
129
|
-
ctx.state.mergeCheckFailures.headline =
|
|
129
|
+
ctx.state.mergeCheckFailures.headline = redCheckHeadline(red.check, red.outcome.outputTail);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
return result;
|
|
@@ -143,15 +143,18 @@ export function noteMergeRetryPassed(ctx, role, pinPatch) {
|
|
|
143
143
|
if (headline)
|
|
144
144
|
warnEventBestEffort(ctx.root, role, MERGE_FLAKY_WARNING_PREFIX + headline);
|
|
145
145
|
}
|
|
146
|
-
/** A merge blocked by a deterministic cross-check
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* under the rebase, and a differently-rebased tree can pass the cross-check — but at the
|
|
146
|
+
/** A merge blocked by a deterministic cross-check — a property of the tree ahead of main,
|
|
147
|
+
* not a flaky run. The block is counted like a red merge check, keyed by the same patch-id:
|
|
148
|
+
* under MERGE_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-merge — main may have
|
|
149
|
+
* moved under the rebase, and a differently-rebased tree can pass the cross-check — but at the
|
|
151
150
|
* limit the block is attributed to the change: rejected deterministically with the block
|
|
152
151
|
* reason, the pin deleted, no model run. Without this a resolution that trips the same
|
|
153
152
|
* cross-check on every re-resolve pays a fresh conflict-resolver run each run, forever
|
|
154
|
-
* (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin).
|
|
153
|
+
* (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). Since part 3m/3
|
|
154
|
+
* the merge path no longer produces cross-check blocks (a fix claim and a broken backlog
|
|
155
|
+
* structure are repairable and land for the main watch), so nothing in the in-lock path calls
|
|
156
|
+
* this today; it stays as the strike-counting sink, still pinned by INV-BF2 and retired with
|
|
157
|
+
* the queue by 3f/3. */
|
|
155
158
|
export async function mergeBlocked(ctx, role, wt, blocked) {
|
|
156
159
|
return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
|
|
157
160
|
}
|
|
@@ -185,7 +188,7 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
|
185
188
|
const kept = keepForKnownFailures(ctx, role, label, red, state);
|
|
186
189
|
if (kept)
|
|
187
190
|
return kept;
|
|
188
|
-
const headline =
|
|
191
|
+
const headline = redCheckHeadline(red.check, red.outcome.outputTail);
|
|
189
192
|
const key = normalizeClusterKey(headline);
|
|
190
193
|
if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
|
|
191
194
|
state.mergeFlakeExcused = key;
|