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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { distinctOrdered } from "../collections.js";
|
|
1
2
|
import { checkMainBaseline } from "./main-baseline.js";
|
|
2
3
|
import { baselineCheckLogger, clearHealKeyOnGreen, withMainTipWorktree } from "./main-red.js";
|
|
3
4
|
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
@@ -13,6 +14,22 @@ import { treeDiffFiles } from "../git/git-diff.js";
|
|
|
13
14
|
import { mergingRoleOf } from "../events/event-read.js";
|
|
14
15
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
15
16
|
import { diffRuleReasons } from "../review/diff-rules.js";
|
|
17
|
+
/** The main_check event payload for a settled verdict: the baseline sha and status, the run
|
|
18
|
+
* duration (0 when nothing ran), the covered-commit count, and, on a red, the failing tests and
|
|
19
|
+
* the quarantined-only marker. */
|
|
20
|
+
function mainCheckEvent(baseline, red, quarantined, covered, durationMs) {
|
|
21
|
+
return {
|
|
22
|
+
source: TUMWATER_SOURCE,
|
|
23
|
+
type: "main_check",
|
|
24
|
+
sha: baseline.sha,
|
|
25
|
+
status: red ? "failed" : "passed",
|
|
26
|
+
durationMs,
|
|
27
|
+
covered,
|
|
28
|
+
...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
|
|
29
|
+
...(quarantined ? { quarantined } : {}),
|
|
30
|
+
...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
16
33
|
/** The failing tests of each root's newest watch verdict, keyed by root. */
|
|
17
34
|
const knownFailures = new Map();
|
|
18
35
|
/** Main's known failures: the failing tests of the watch's newest verdict — main's tip, or the
|
|
@@ -20,7 +37,7 @@ const knownFailures = new Map();
|
|
|
20
37
|
* (review/known-flakes.ts). Empty when the verdict is green, red with no parsed test, or none has
|
|
21
38
|
* been recorded yet in this process, and nothing is quarantined. */
|
|
22
39
|
export function knownMainFailures(root) {
|
|
23
|
-
return
|
|
40
|
+
return distinctOrdered([...(knownFailures.get(root) ?? []), ...quarantinedTests(root)]);
|
|
24
41
|
}
|
|
25
42
|
/** The failing tests of a failed check when every one of them is a known main failure; null when
|
|
26
43
|
* the output names no test, any test is outside the known set, or none is known. A list at
|
|
@@ -76,8 +93,9 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
async function watchOnce(mainHead, config) {
|
|
79
|
-
// The deterministic diff rules ("Every commit is verified on main, part 1/3"): run the
|
|
80
|
-
// five
|
|
96
|
+
// The deterministic diff rules ("Every commit is verified on main, part 1/3"): run the full
|
|
97
|
+
// five-check set the watch and gate share — the gate now runs only the three a later commit
|
|
98
|
+
// cannot repair — against main's new tip. A rule failure is a
|
|
81
99
|
// deterministic red — it takes no confirming re-run and never quarantines — reported as a
|
|
82
100
|
// warning naming the rule and the head (and each committing role, when a recent merged event
|
|
83
101
|
// names one). It runs on every tip the watch covers in a poll — a build green, a build
|
|
@@ -106,7 +124,7 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
106
124
|
await git(wt, "checkout", "-q", "--detach", "--force", commit);
|
|
107
125
|
const parent = `${commit}^`;
|
|
108
126
|
const files = (await treeDiffFiles(wt, parent, commit)) ?? [];
|
|
109
|
-
const [ruleReason] = await diffRuleReasons(wt, parent, files, mergingRoleOf(root, commit) ?? "", isExemptDiff(files, config.review.exemptPaths));
|
|
127
|
+
const [ruleReason] = await diffRuleReasons(wt, parent, files, mergingRoleOf(root, commit) ?? "", isExemptDiff(files, config.review.exemptPaths), "watch");
|
|
110
128
|
if (ruleReason !== undefined) {
|
|
111
129
|
logEventBestEffort(root, {
|
|
112
130
|
source: TUMWATER_SOURCE,
|
|
@@ -120,17 +138,11 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
120
138
|
}
|
|
121
139
|
}
|
|
122
140
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
lastWatched = mainHead; // no declared check: nothing else to watch until main moves
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
133
|
-
const started = Date.now();
|
|
141
|
+
/** Run the declared build check on main's tip, re-running a red once and quarantining a flake
|
|
142
|
+
* whose re-run turns green. Returns the check's result and whether any run actually executed
|
|
143
|
+
* (false for a cache hit, a skip, or a sleep-spanned run). `config` and `root` are captured
|
|
144
|
+
* from `watchOnce`. */
|
|
145
|
+
async function runBaselineCheck(wt) {
|
|
134
146
|
let ran = false;
|
|
135
147
|
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
136
148
|
const hook = (run) => {
|
|
@@ -145,45 +157,31 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
145
157
|
if (check.baseline?.status === "green")
|
|
146
158
|
quarantineTests(root, first.failingTests, (first.failingTests ?? []).join("\n"));
|
|
147
159
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
return { check, ran };
|
|
161
|
+
}
|
|
162
|
+
/** The covered-commit count for a main_check event: the first-parent commits between the
|
|
163
|
+
* previous and new watched heads, or 1 when there is no previous head. */
|
|
164
|
+
async function countCovered(wt, previous, sha) {
|
|
165
|
+
if (previous === undefined)
|
|
166
|
+
return 1;
|
|
167
|
+
const counted = Number(((await gitTry(wt, "rev-list", "--first-parent", "--count", `${previous}..${sha}`)) ?? "").trim());
|
|
168
|
+
return Number.isFinite(counted) && counted > 0 ? counted : 1;
|
|
169
|
+
}
|
|
170
|
+
/** Settle the verdict for a checked baseline: distinguish quarantined-only reds, record
|
|
171
|
+
* main's known failures, count the covered commits, and log the main_check event. Returns
|
|
172
|
+
* whether the tip settled red (a red naming tests that are not all quarantined). */
|
|
173
|
+
async function recordMainCheck(wt, baseline, previous, ran, started) {
|
|
158
174
|
// A red naming only quarantined tests counts as green: no redeploy hold.
|
|
159
175
|
const quarantined = baseline.status === "red" ? onlyQuarantined(root, baseline.failingTests) : null;
|
|
160
176
|
const red = baseline.status === "red" && !quarantined;
|
|
161
177
|
knownFailures.set(root, red ? (baseline.failingTests ?? []) : []);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
status: red ? "failed" : "passed",
|
|
170
|
-
durationMs: ran ? Date.now() - started : 0,
|
|
171
|
-
covered: Number.isFinite(counted) && counted > 0 ? counted : 1,
|
|
172
|
-
...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
|
|
173
|
-
...(quarantined ? { quarantined } : {}),
|
|
174
|
-
...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
|
|
175
|
-
});
|
|
176
|
-
// The diff rules judge the tip whether the build is green or red; the build-red path below
|
|
177
|
-
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
178
|
-
await watchRules(wt, previous, baseline.sha);
|
|
179
|
-
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
180
|
-
// heal-dedup key, so a failing set that returns after a green main is healed again. A
|
|
181
|
-
// settled red verdict below does NOT clear it: the heal-dedup key stays set while main
|
|
182
|
-
// is red.
|
|
183
|
-
if (!red) {
|
|
184
|
-
clearHealKeyOnGreen();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
178
|
+
const covered = await countCovered(wt, previous, baseline.sha);
|
|
179
|
+
logEventBestEffort(root, mainCheckEvent(baseline, red, quarantined, covered, ran ? Date.now() - started : 0));
|
|
180
|
+
return red;
|
|
181
|
+
}
|
|
182
|
+
/** Name a settled red tip as the culprit: its merging runner, the main_culprit event, and
|
|
183
|
+
* the onCulprit callback with the tip and its failing tests. */
|
|
184
|
+
function reportCulprit(baseline) {
|
|
187
185
|
// The newest head is the culprit ("Every commit is verified on main, part 2/3"): the watch
|
|
188
186
|
// checks each new red tip, so whatever it found belongs to the head it checked. Once per red
|
|
189
187
|
// head — lastWatched already holds it, so a later poll does not attribute it again — name
|
|
@@ -201,6 +199,42 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
201
199
|
failingTests: baseline.failingTests ?? [],
|
|
202
200
|
tip: baseline.sha,
|
|
203
201
|
});
|
|
202
|
+
}
|
|
203
|
+
if (!detectBuildCheck(root, config)) {
|
|
204
|
+
// No declared build check: nothing to watch for a build red, but the diff rules still run on
|
|
205
|
+
// the tip. The build-red path below is what never runs here.
|
|
206
|
+
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
207
|
+
await watchRules(wt, lastWatched, await headOf(wt));
|
|
208
|
+
});
|
|
209
|
+
lastWatched = mainHead; // no declared check: nothing else to watch until main moves
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
213
|
+
const started = Date.now();
|
|
214
|
+
const { check, ran } = await runBaselineCheck(wt);
|
|
215
|
+
const baseline = check.baseline;
|
|
216
|
+
const previous = lastWatched;
|
|
217
|
+
// A skipped or sleep-spanned run made no verdict: the head still counts as watched, so the
|
|
218
|
+
// next poll does not start another run for it — but the diff rules still run on this tip.
|
|
219
|
+
if (!baseline) {
|
|
220
|
+
const tip = await headOf(wt);
|
|
221
|
+
await watchRules(wt, previous, tip);
|
|
222
|
+
return void (lastWatched = tip);
|
|
223
|
+
}
|
|
224
|
+
lastWatched = baseline.sha;
|
|
225
|
+
const red = await recordMainCheck(wt, baseline, previous, ran, started);
|
|
226
|
+
// The diff rules judge the tip whether the build is green or red; the build-red path below
|
|
227
|
+
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
228
|
+
await watchRules(wt, previous, baseline.sha);
|
|
229
|
+
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
230
|
+
// heal-dedup key, so a failing set that returns after a green main is healed again. A
|
|
231
|
+
// settled red verdict below does NOT clear it: the heal-dedup key stays set while main
|
|
232
|
+
// is red.
|
|
233
|
+
if (!red) {
|
|
234
|
+
clearHealKeyOnGreen();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
reportCulprit(baseline);
|
|
204
238
|
});
|
|
205
239
|
}
|
|
206
240
|
return {
|
package/dist/src/brief/brief.js
CHANGED
|
@@ -106,16 +106,10 @@ export function briefFile(root) {
|
|
|
106
106
|
* on-purpose bare repo. An unreadable candidate counts: a brief that cannot be read is one
|
|
107
107
|
* the roles cannot use, and the underlying firstBriefText throw is caught and reported as
|
|
108
108
|
* true here — same subset doctor's `brief` warn covers, minus the bare-repo state that
|
|
109
|
-
* doctor deems fine.
|
|
109
|
+
* doctor deems fine. Thin accessor over briefStatus, so this predicate and the snapshot's
|
|
110
|
+
* shared flags share one implementation. */
|
|
110
111
|
export function briefBlockMissing(root) {
|
|
111
|
-
|
|
112
|
-
return false;
|
|
113
|
-
try {
|
|
114
|
-
return (readBrief(root)?.prompt ?? "") === "";
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
112
|
+
return briefStatus(root).missing;
|
|
119
113
|
}
|
|
120
114
|
/** True when the resolved brief block exceeds the read cap (BRIEF_TEXT_MAX_CHARS) — the
|
|
121
115
|
* always-on amber twin of the doctor `brief` row's over-cap warn. extractPrompt truncates
|
|
@@ -127,15 +121,34 @@ export function briefBlockMissing(root) {
|
|
|
127
121
|
* a surrogate pair, while an at-cap block stays equal to it. A repo with no candidate
|
|
128
122
|
* resolves null (0, false), and an unreadable candidate's readBrief throw is caught to false,
|
|
129
123
|
* because briefBlockMissing already raises `no-brief` for that state; an empty block stays
|
|
130
|
-
* briefBlockMissing's (its resolved length is 0).
|
|
131
|
-
* snapshot. */
|
|
124
|
+
* briefBlockMissing's (its resolved length is 0). Thin accessor over briefStatus, so this
|
|
125
|
+
* predicate and the snapshot's shared flags share one implementation. */
|
|
132
126
|
export function briefBlockOverCap(root) {
|
|
127
|
+
return briefStatus(root).overCap;
|
|
128
|
+
}
|
|
129
|
+
/** The always-on brief degraded-state flags, derived from ONE candidate scan and ONE file
|
|
130
|
+
* read — the per-second status snapshot's `noBrief`/`briefTruncated` fields, and the single
|
|
131
|
+
* implementation of both predicates: the standalone briefBlockMissing/briefBlockOverCap
|
|
132
|
+
* accessors delegate here, so the shared and standalone paths cannot disagree. The snapshot
|
|
133
|
+
* used to call those two accessors, each calling readBrief — which re-lists both candidates
|
|
134
|
+
* and re-reads the whole brief file — twice per poll, on a file that changes only when a role
|
|
135
|
+
* edits it. briefStatus is the degraded-state twin of readBrief's promise (briefFile +
|
|
136
|
+
* readBriefText derive from one scan): the two flags share one read, and each flag applies the
|
|
137
|
+
* value's own predicate. `missing` is true when a candidate exists but none resolves a
|
|
138
|
+
* non-empty block (or the resolving read threw); `overCap` true when the resolved block
|
|
139
|
+
* exceeds the cap. A repo with no candidate at all is neither (both false). */
|
|
140
|
+
export function briefStatus(root) {
|
|
141
|
+
if (briefCandidates(root).every((candidate) => !fs.existsSync(candidate))) {
|
|
142
|
+
return { missing: false, overCap: false };
|
|
143
|
+
}
|
|
144
|
+
let prompt = "";
|
|
133
145
|
try {
|
|
134
|
-
|
|
146
|
+
prompt = readBrief(root)?.prompt ?? "";
|
|
135
147
|
}
|
|
136
148
|
catch {
|
|
137
|
-
return false;
|
|
149
|
+
return { missing: true, overCap: false }; // Unreadable — the briefBlockMissing subset.
|
|
138
150
|
}
|
|
151
|
+
return { missing: prompt === "", overCap: prompt.length > BRIEF_TEXT_MAX_CHARS };
|
|
139
152
|
}
|
|
140
153
|
/** The project's brief text, extracted from the resolved brief file's managed section —
|
|
141
154
|
* TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
|
|
@@ -152,11 +152,10 @@ export function modelPairName(p) {
|
|
|
152
152
|
* `strong` tiers resolved to a usable free pair (resolveTierFallbacks over the full usable
|
|
153
153
|
* predicate — price AND breaker serving) — plus whether the review gate is on. The gate is
|
|
154
154
|
* `paused` when `default` resolves to pause (nothing for the team's bulk to run on), or when
|
|
155
|
-
* review is on and `strong` does
|
|
156
|
-
* their
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* display it, so what an operator sees is what the scheduler is doing. */
|
|
155
|
+
* review is on and `strong` does; `fallback` otherwise, with the tiers that did resolve running
|
|
156
|
+
* their pairs and the rest held per role. This is the value the dashboards display; the
|
|
157
|
+
* orchestrator's role holds come from the per-tier set `budgetPausedRoles` (a tier whose own
|
|
158
|
+
* pair did not resolve), not from a reviewer-derived team-wide pause. */
|
|
160
159
|
export function budgetGate(reached, tiers, reviewOn) {
|
|
161
160
|
if (!reached)
|
|
162
161
|
return "open";
|
|
@@ -125,6 +125,16 @@ export function failureHeadline(tail) {
|
|
|
125
125
|
export function describeCheck(check) {
|
|
126
126
|
return check.kind === "npm" ? `\`npm run ${check.script}\`` : `\`${check.command}\``;
|
|
127
127
|
}
|
|
128
|
+
/** The single-line headline of a red check — the failureHeadline when the tail has one to
|
|
129
|
+
* offer (a test name or marker plus a non-framing message), else the check's describeCheck
|
|
130
|
+
* command name — the name the gate, merge, and review warnings print for a failed check.
|
|
131
|
+
* One definition for every red that names itself to a human: review-precheck's two
|
|
132
|
+
* flake/failure warnings, merge-to-main's merge-retry quarantine warning, and
|
|
133
|
+
* merge-check-failures's kept-pin headline record and attributeRedCheck absent-tail headline.
|
|
134
|
+
*/
|
|
135
|
+
export function redCheckHeadline(check, tail) {
|
|
136
|
+
return failureHeadline(tail) ?? describeCheck(check);
|
|
137
|
+
}
|
|
128
138
|
/** The machine-generated reasons a red check hands its author: the headline joined to the rest of
|
|
129
139
|
* the clipped tail, so the compiler error sits right after it in the injected next-run note. The
|
|
130
140
|
* headline is failureHeadline's — the first line that is not a stack frame — not outputTail[0]: a
|
|
@@ -33,8 +33,6 @@ function resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad) {
|
|
|
33
33
|
* the one-permit-two-attempt rule. Prices the first attempt as its own build_check event and
|
|
34
34
|
* returns the final outcome and its run duration. */
|
|
35
35
|
async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper, signal) {
|
|
36
|
-
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
37
|
-
// holds); durationMs prices the run itself, not the wait for a permit.
|
|
38
36
|
let durationMs = 0;
|
|
39
37
|
const raw = await withCheckPermit(config, MERGE_SCOPES.has(scope) ? CHECK_TIER.merge : CHECK_TIER.other, async () => {
|
|
40
38
|
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
@@ -213,7 +213,8 @@ export function swapDist(root, dist, mainHead) {
|
|
|
213
213
|
* swap can close the window, because the survivor is the supervisor and it exits on the dead
|
|
214
214
|
* generation; the next `tumwater start` then cannot even load `dist/src/cli.js` and fails with
|
|
215
215
|
* MODULE_NOT_FOUND, so the repo is wedged until someone renames `dist.prev` back by hand. The
|
|
216
|
-
* supervisor calls this before it exits (src/cli/cli-start.ts, via superviseStart's
|
|
216
|
+
* supervisor calls this before it exits (src/cli/cli-start-supervisor.ts, via superviseStart's
|
|
217
|
+
* onDone): while
|
|
217
218
|
* it is still up, it renames the preserved old build back into place so the next boot works.
|
|
218
219
|
*
|
|
219
220
|
* Only acts when `dist` is missing and a `dist.prev` exists — both conditions together are the
|
|
@@ -228,11 +229,12 @@ export function recoverInterruptedSwap(root, dist) {
|
|
|
228
229
|
fs.renameSync(prev, dist); // Atomic: dist is either the restored old build or still missing.
|
|
229
230
|
return true;
|
|
230
231
|
}
|
|
231
|
-
/** The supervisor's end-of-supervision repair (src/cli/cli-start.ts wires this into
|
|
232
|
+
/** The supervisor's end-of-supervision repair (src/cli/cli-start-supervisor.ts wires this into
|
|
232
233
|
* superviseStart's onDone): recover dist/ after an interrupted swap and, either way, put the
|
|
233
234
|
* outcome on the event feed. Kept beside recoverInterruptedSwap so the CLI wiring is one call.
|
|
234
|
-
* cli-start.ts imports this module statically, so the code is in memory before a
|
|
235
|
-
* dist/ away — recovering via an import at call time would fail
|
|
235
|
+
* cli-start-supervisor.ts imports this module statically, so the code is in memory before a
|
|
236
|
+
* swap can move dist/ away — recovering via an import at call time would fail
|
|
237
|
+
* ERR_MODULE_NOT_FOUND exactly
|
|
236
238
|
* when it is needed, the window this closes. `dist` defaults to this process's own dist
|
|
237
239
|
* (build-info.distDir, a pure path computation captured at load), overridable so a test can
|
|
238
240
|
* point it at a temp root. A failed restore is reported, never thrown: the hook runs on the
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
|
|
10
10
|
import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
|
|
11
11
|
import { gitTry } from "../git/git-run.js";
|
|
12
|
-
import { aheadOfMainDiff, aheadOfMainFiles, changedFiles } from "../git/git-diff.js";
|
|
12
|
+
import { aheadOfMainDiff, aheadOfMainFiles, changedFiles, DIFF_MAX_BYTES } from "../git/git-diff.js";
|
|
13
13
|
import { compareCodeUnits } from "../collections.js";
|
|
14
14
|
import { isAbsolute } from "node:path";
|
|
15
15
|
import { branchName } from "../paths.js";
|
|
@@ -44,9 +44,6 @@ export async function collectRoleChanges(root, roles, opts = {}) {
|
|
|
44
44
|
const views = await Promise.all(roles.map((role) => collectRoleChangeAt(root, role, scope, opts)));
|
|
45
45
|
return { mainBranch: scope.mainBranch, roles: views };
|
|
46
46
|
}
|
|
47
|
-
/** The uncommitted patch's cap, matching aheadOfMainDiff's — one shared bound so neither
|
|
48
|
-
* half of the view can print unbounded text. */
|
|
49
|
-
const DIFF_MAX_BYTES = 200_000;
|
|
50
47
|
/** Collect the change the role holds: both halves of the view, or a degraded state
|
|
51
48
|
* when the worktree or the base branch is missing. Never throws on a degraded team — a
|
|
52
49
|
* query about a role that has not run yet must answer, not fail (report's rationale). */
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { completionConfigKeys, completionModels, completionRoleIds, completionScript, configValueWords, SUPPORTED_SHELLS, } from "./completion.js";
|
|
2
|
+
import { say, fail } from "./cli-output.js";
|
|
3
|
+
import { gotSuffix } from "../text/text.js";
|
|
4
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
5
|
+
/** `tumwater completion`'s command body — the shell-completion script and its --roles/
|
|
6
|
+
* --config-keys/--config-value/--models listing shapes. The completion data lives in
|
|
7
|
+
* src/cli/completion.ts (the spec parser, the completionScript emitter and the read-only
|
|
8
|
+
* listing helpers); this module answers a `completion` invocation, split out of cli.ts so the
|
|
9
|
+
* dispatcher stays the pure shared sequence and this command body sits beside its data the
|
|
10
|
+
* way every other command body does. Imported lazily by cli.ts like each command body. */
|
|
11
|
+
function printCompletionRoles(root) {
|
|
12
|
+
for (const id of completionRoleIds(root))
|
|
13
|
+
say(id);
|
|
14
|
+
}
|
|
15
|
+
function printCompletionConfigKeys(root) {
|
|
16
|
+
for (const key of completionConfigKeys(root))
|
|
17
|
+
say(key);
|
|
18
|
+
}
|
|
19
|
+
function printCompletionConfigValue(key) {
|
|
20
|
+
// The vocabulary `config set <key> <value>` completes for a discrete-valued key
|
|
21
|
+
// (boolean/backend/model/thinking); free-form keys print nothing, so a `set` on one
|
|
22
|
+
// completes nothing instead of offering a wrong vocabulary.
|
|
23
|
+
for (const word of configValueWords(key))
|
|
24
|
+
say(word);
|
|
25
|
+
}
|
|
26
|
+
function printCompletionModels(root) {
|
|
27
|
+
for (const m of completionModels(root))
|
|
28
|
+
say(m);
|
|
29
|
+
}
|
|
30
|
+
const COMPLETION_LISTING_SHAPES = {
|
|
31
|
+
"--roles": "roles",
|
|
32
|
+
"--config-keys": "config-keys",
|
|
33
|
+
"--models": "models",
|
|
34
|
+
};
|
|
35
|
+
function completionListShape(args) {
|
|
36
|
+
if (args.length === 1 && args[0] in COMPLETION_LISTING_SHAPES)
|
|
37
|
+
return COMPLETION_LISTING_SHAPES[args[0]];
|
|
38
|
+
if (args.length === 2 && args[0] === "--config-value")
|
|
39
|
+
return "config-value";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function completionShell(args) {
|
|
43
|
+
const shell = args[0];
|
|
44
|
+
// A wrong word count names the count, not a bad shell. A single near-miss shell word gets
|
|
45
|
+
// the same typoSuffix `logs --type`/`history --result` give, so `completion zs` reads
|
|
46
|
+
// "did you mean `zsh`?" while a far-off value (`tcsh`) stays hint-free.
|
|
47
|
+
if (args.length === 0)
|
|
48
|
+
fail("completion needs a shell: bash, zsh, or fish");
|
|
49
|
+
if (args.length > 1)
|
|
50
|
+
fail(`completion takes one shell: bash, zsh, or fish (got ${args.length} words: ${args.join(" ")})`);
|
|
51
|
+
if (!SUPPORTED_SHELLS.includes(shell))
|
|
52
|
+
fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(shell)}${typoSuffix(shell ?? "", SUPPORTED_SHELLS)}`);
|
|
53
|
+
return shell;
|
|
54
|
+
}
|
|
55
|
+
/** Answer a `tumwater completion` invocation. Works outside a repo (no requireReadyRepo). */
|
|
56
|
+
export async function cmdCompletion(root, args) {
|
|
57
|
+
const shape = completionListShape(args);
|
|
58
|
+
if (shape === "roles") {
|
|
59
|
+
printCompletionRoles(root);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (shape === "config-keys") {
|
|
63
|
+
printCompletionConfigKeys(root);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (shape === "config-value") {
|
|
67
|
+
printCompletionConfigValue(args[1]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (shape === "models") {
|
|
71
|
+
printCompletionModels(root);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
say(completionScript(completionShell(args)));
|
|
75
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { rejectUnknownArgs, JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG, } from "./cli-flag-specs.js";
|
|
2
|
+
import { say, sayJsonOrRender } from "./cli-output.js";
|
|
3
|
+
import { parseDoctorChecks, peelPositionals } from "./cli-command-args.js";
|
|
4
|
+
import { loadConfigSafe } from "../config/config.js";
|
|
5
|
+
/** Run one doctor side-effect (`--kill-orphans`, `--commit-backlog`, `--resync-install`,
|
|
6
|
+
* `--drop-broken-pins`, `--retire-leftovers`) and return its report payload for the optional
|
|
7
|
+
* `--json` run — the shape every doctor*Case side-effect wrapper follows: import the effect's
|
|
8
|
+
* module and run it only when its flag is present, then (text form only) print the effect's
|
|
9
|
+
* own lines before the report with a separating blank line when there is anything to print;
|
|
10
|
+
* `--json` keeps stdout one document, carrying the result on the payload instead. The lazy
|
|
11
|
+
* per-effect `load` keeps each module imported only when its flag is asked for. */
|
|
12
|
+
async function doctorSideEffect(args, flag, load) {
|
|
13
|
+
if (!args.includes(flag))
|
|
14
|
+
return undefined;
|
|
15
|
+
const { run, lines } = await load();
|
|
16
|
+
const payload = await run();
|
|
17
|
+
// The text form prints the effect before the report; --json keeps stdout one document.
|
|
18
|
+
if (!args.includes("--json")) {
|
|
19
|
+
const out = lines(payload);
|
|
20
|
+
for (const line of out)
|
|
21
|
+
say(line);
|
|
22
|
+
if (out.length > 0)
|
|
23
|
+
say("");
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
async function doctorKillOrphans(root, args) {
|
|
28
|
+
return doctorSideEffect(args, "--kill-orphans", async () => {
|
|
29
|
+
const { killOrphans, orphanKillLines } = await import("../doctor/doctor-orphans.js");
|
|
30
|
+
return { run: () => killOrphans(root), lines: orphanKillLines };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function doctorCommitBacklog(root, args) {
|
|
34
|
+
return doctorSideEffect(args, "--commit-backlog", async () => {
|
|
35
|
+
const { commitUncommittedBacklog, backlogCommitLines } = await import("../doctor/doctor-backlog.js");
|
|
36
|
+
const { config } = loadConfigSafe(root); // the report's own config, so base resolution and the off-base skip match the `uncommitted backlog` row's
|
|
37
|
+
return { run: () => commitUncommittedBacklog(root, config), lines: backlogCommitLines };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function doctorResyncInstall(root, args) {
|
|
41
|
+
return doctorSideEffect(args, "--resync-install", async () => {
|
|
42
|
+
const { resyncRootInstall, rootInstallResyncLines } = await import("../build/dep-install.js");
|
|
43
|
+
return { run: () => resyncRootInstall(root), lines: rootInstallResyncLines };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function doctorDropBrokenPins(root, args) {
|
|
47
|
+
return doctorSideEffect(args, "--drop-broken-pins", async () => {
|
|
48
|
+
const { dropBrokenPins, brokenPinDropLines } = await import("../doctor/doctor-contention-checks.js");
|
|
49
|
+
return { run: () => dropBrokenPins(root), lines: brokenPinDropLines };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function doctorRetireLeftovers(root, args) {
|
|
53
|
+
return doctorSideEffect(args, "--retire-leftovers", async () => {
|
|
54
|
+
const { retireLeftovers, leftoverRetireLines } = await import("../doctor/doctor-contention-checks.js");
|
|
55
|
+
const { config } = loadConfigSafe(root); // the row's own config, so the reap and the report agree on what is retirable
|
|
56
|
+
return { run: () => retireLeftovers(root, config), lines: leftoverRetireLines };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function doctorPayload(payload, parts) {
|
|
60
|
+
const out = { ...payload };
|
|
61
|
+
if (parts.leftoverRetire !== undefined)
|
|
62
|
+
out.leftoverRetire = parts.leftoverRetire;
|
|
63
|
+
if (parts.brokenPinDrop !== undefined)
|
|
64
|
+
out.brokenPinDrop = parts.brokenPinDrop;
|
|
65
|
+
if (parts.resyncInstall !== undefined)
|
|
66
|
+
out.resyncInstall = parts.resyncInstall;
|
|
67
|
+
if (parts.backlogCommit !== undefined)
|
|
68
|
+
out.backlogCommit = parts.backlogCommit;
|
|
69
|
+
if (parts.orphanKill !== undefined)
|
|
70
|
+
out.orphanKill = parts.orphanKill;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** `tumwater doctor`: report what in the environment is keeping the team from running, and
|
|
74
|
+
* run the optional repair side-effects. No ready-repo gate: doctor's job is to report WHY the
|
|
75
|
+
* environment isn't ready, so it must run outside a git repo and print fail lines rather than
|
|
76
|
+
* throwing. */
|
|
77
|
+
export async function cmdDoctor(root, args) {
|
|
78
|
+
const { positionals: checkNames, rest: doctorFlags } = peelPositionals(args);
|
|
79
|
+
rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG]);
|
|
80
|
+
const only = parseDoctorChecks(checkNames);
|
|
81
|
+
const { runDoctor } = await import("../doctor/doctor.js");
|
|
82
|
+
const parts = {
|
|
83
|
+
orphanKill: await doctorKillOrphans(root, args),
|
|
84
|
+
backlogCommit: await doctorCommitBacklog(root, args),
|
|
85
|
+
resyncInstall: await doctorResyncInstall(root, args),
|
|
86
|
+
brokenPinDrop: await doctorDropBrokenPins(root, args),
|
|
87
|
+
leftoverRetire: await doctorRetireLeftovers(root, args),
|
|
88
|
+
};
|
|
89
|
+
const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("../doctor/doctor-render.js");
|
|
90
|
+
const report = await runDoctor(root, undefined, undefined, only);
|
|
91
|
+
// --json prints the collector's own payload (the DoctorReport object), not a re-parse of
|
|
92
|
+
// the render — the `report --json` precedent. The exit-code contract below holds in both
|
|
93
|
+
// forms: 1 when any check fails, 0 otherwise; warnings never fail the exit.
|
|
94
|
+
// --problems keeps the full report for the render (it counts the hidden ok rows) and
|
|
95
|
+
// hands --json the filtered one; the exit code below reads the full report either way.
|
|
96
|
+
// --kill-orphans adds the kill result to the JSON object as `orphanKill`.
|
|
97
|
+
if (args.includes("--problems"))
|
|
98
|
+
sayJsonOrRender(args, args.includes("--json") ? doctorPayload(problemsOnly(report), parts) : report, renderDoctorProblems);
|
|
99
|
+
else
|
|
100
|
+
sayJsonOrRender(args, args.includes("--json") ? doctorPayload(report, parts) : report, renderDoctor);
|
|
101
|
+
if (report.checks.some((c) => c.level === "fail"))
|
|
102
|
+
process.exitCode = 1; // Warnings never fail the exit.
|
|
103
|
+
}
|
package/dist/src/cli/cli-init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `tumwater init`'s implementation (src/init/init.ts does the work; this prints the report).
|
|
2
2
|
* Split out of cli/cli-start.ts, whose name is for start alone: every command it dispatches
|
|
3
|
-
* delegates to its own module (
|
|
4
|
-
*
|
|
3
|
+
* delegates to its own module (cli-config-commands.ts, cli-log-commands.ts, doctor.ts, …), and init
|
|
4
|
+
* is no exception. cli.ts stays the pure dispatcher. */
|
|
5
5
|
import { say } from "./cli-output.js";
|
|
6
6
|
import { parseInitArgs } from "./cli-command-args.js";
|
|
7
7
|
import { initProject } from "../init/init.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The read-only query commands' bodies (status, diff, queue, backlog), split out of
|
|
2
2
|
* cli.ts's dispatcher: main() routes to a cmd* here exactly as it routes to operator-commands's
|
|
3
|
-
* cmdAbort or config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
|
|
3
|
+
* cmdAbort or cli-config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
|
|
4
4
|
* surfaces. Each command owns its argument gate, ready-repo gating decision, and output shape,
|
|
5
5
|
* with the wording and gating order moved verbatim from main()'s cases. */
|
|
6
6
|
import { fail, say, sayJson, sayJsonOrRender } from "./cli-output.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `tumwater role <id>`'s command body, split out of cli-query-commands.ts so that module
|
|
2
2
|
* keeps only the read-only query commands (status, diff, queue, backlog). cli.ts's dispatcher
|
|
3
3
|
* routes to cmdRole here exactly as it routes cmdStatus to cli-query-commands or cmdConfig to
|
|
4
|
-
* config-commands. cmdRole is the one query-named command that also writes: it renders a
|
|
4
|
+
* cli-config-commands. cmdRole is the one query-named command that also writes: it renders a
|
|
5
5
|
* role's standing and next-run prompts and carries eleven per-role write flags (notes,
|
|
6
6
|
* instructions, quiet-hours, daily cap, min-run-interval, instances, enable/disable, and the
|
|
7
7
|
* provider/model/thinking/backend), each through the same setConfigKey/unsetConfigKey core
|
|
@@ -56,10 +56,6 @@ const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" }
|
|
|
56
56
|
* roles.<base>.enabled. */
|
|
57
57
|
const ENABLE_SPEC = { names: ["--enable"] };
|
|
58
58
|
const DISABLE_SPEC = { names: ["--disable"] };
|
|
59
|
-
/** `role`'s review-after-merge write flags: add the role to, or remove it from,
|
|
60
|
-
* review.afterMerge (written for the base id). */
|
|
61
|
-
const REVIEW_AFTER_MERGE_SPEC = { names: ["--review-after-merge"] };
|
|
62
|
-
const CLEAR_REVIEW_AFTER_MERGE_SPEC = { names: ["--clear-review-after-merge"] };
|
|
63
59
|
/** `role`'s model-triple write flags: clear the field, or set it, written to
|
|
64
60
|
* roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
|
|
65
61
|
* pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
|
|
@@ -175,7 +171,7 @@ export async function cmdRole(root, args) {
|
|
|
175
171
|
if (next?.startsWith("--"))
|
|
176
172
|
fail(`${flag} needs a value (the next token ${JSON.stringify(next)} is a flag, not a value)`);
|
|
177
173
|
}
|
|
178
|
-
rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC, CLEAR_BACKEND_SPEC, BACKEND_SPEC
|
|
174
|
+
rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC, CLEAR_BACKEND_SPEC, BACKEND_SPEC]);
|
|
179
175
|
refuseTeamWorkspace(root);
|
|
180
176
|
// The id is required: positional (`tumwater role <id>`) or --role <id> (the flag
|
|
181
177
|
// spelling every other role-targeting command shares). Both at once is a mistake, not
|
|
@@ -211,9 +207,6 @@ export async function cmdRole(root, args) {
|
|
|
211
207
|
const backendWrite = clearBackend || rest.includes("--backend");
|
|
212
208
|
const enable = rest.includes("--enable");
|
|
213
209
|
const disable = rest.includes("--disable");
|
|
214
|
-
const reviewAfterMerge = rest.includes("--review-after-merge");
|
|
215
|
-
const clearReviewAfterMerge = rest.includes("--clear-review-after-merge");
|
|
216
|
-
const reviewAfterMergeWrite = reviewAfterMerge || clearReviewAfterMerge;
|
|
217
210
|
const writes = [];
|
|
218
211
|
if (notesWrite)
|
|
219
212
|
writes.push("notes");
|
|
@@ -239,8 +232,6 @@ export async function cmdRole(root, args) {
|
|
|
239
232
|
writes.push("enable");
|
|
240
233
|
if (disable)
|
|
241
234
|
writes.push("disable");
|
|
242
|
-
if (reviewAfterMergeWrite)
|
|
243
|
-
writes.push("review-after-merge");
|
|
244
235
|
if (writes.length > 1)
|
|
245
236
|
fail(`${writes.join(" and ")} writes cannot share one invocation`);
|
|
246
237
|
if (notesWrite) {
|
|
@@ -443,36 +434,6 @@ export async function cmdRole(root, args) {
|
|
|
443
434
|
say(`${base}: ${state}`);
|
|
444
435
|
return;
|
|
445
436
|
}
|
|
446
|
-
// The review-after-merge write: the one command the post-merge churn row's remedy
|
|
447
|
-
// needs — add the role to, or remove it from, review.afterMerge for the base id, the
|
|
448
|
-
// membership test reviewedAfterMerge shares. The whole-list value lands through the same
|
|
449
|
-
// setConfigKey core `config set review.afterMerge <json>` uses, so validateConfig owns the
|
|
450
|
-
// string-array shape, the known-role-id check and the `review.afterMerge must not be
|
|
451
|
-
// "director"` refusal, and the write is atomic. A role already in the asked-for state is
|
|
452
|
-
// reported as such and the file is left alone (the section branch compares the value
|
|
453
|
-
// against the resolved list, so a no-op reports unchanged and writes nothing).
|
|
454
|
-
if (reviewAfterMergeWrite) {
|
|
455
|
-
if (reviewAfterMerge && clearReviewAfterMerge)
|
|
456
|
-
fail("give --review-after-merge or --clear-review-after-merge, not both");
|
|
457
|
-
if (rest.includes("--json"))
|
|
458
|
-
fail("--json does not apply to a review-after-merge write");
|
|
459
|
-
requireKnownRole(root, id);
|
|
460
|
-
const base = baseRoleOf(id);
|
|
461
|
-
const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
|
|
462
|
-
const has = list.includes(base);
|
|
463
|
-
const next = has === reviewAfterMerge ? list : reviewAfterMerge ? [...list, base] : list.filter((r) => r !== base);
|
|
464
|
-
const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
|
|
465
|
-
if (!result.ok)
|
|
466
|
-
fail(result.error);
|
|
467
|
-
say(result.unchanged
|
|
468
|
-
? reviewAfterMerge
|
|
469
|
-
? `${base}: already in review.afterMerge, so tumwater.json was not written`
|
|
470
|
-
: `${base}: not in review.afterMerge, so nothing was cleared`
|
|
471
|
-
: reviewAfterMerge
|
|
472
|
-
? `${base}: added to review.afterMerge`
|
|
473
|
-
: `${base}: removed from review.afterMerge`);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
437
|
// The unknown-role answer (exit 1, unknownRoleMessage's wording) lives in the
|
|
477
438
|
// collector; here the id just has to be non-null for the thunk's types.
|
|
478
439
|
sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
|