tumwater 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +13 -0
- package/dist/src/agent/claude/claude-backend.js +2 -2
- package/dist/src/agent/claude/claude-stream.js +3 -2
- package/dist/src/agent/codex/codex-backend.js +3 -3
- package/dist/src/agent/codex/codex-stream.js +3 -1
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/backlog/backlog-archive.js +60 -25
- package/dist/src/backlog/backlog-cli-view.js +3 -28
- package/dist/src/backlog/backlog-cli.js +9 -64
- package/dist/src/backlog/backlog-commit.js +17 -9
- package/dist/src/backlog/backlog-eligibility.js +44 -26
- package/dist/src/backlog/backlog-parts.js +23 -13
- package/dist/src/backlog/backlog-release.js +33 -16
- package/dist/src/baseline/main-guardrails.js +0 -73
- package/dist/src/baseline/main-red.js +31 -21
- package/dist/src/baseline/main-watch.js +82 -34
- package/dist/src/brief/brief-edit.js +3 -1
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/dep-install.js +64 -1
- package/dist/src/cli/cli-args.js +63 -0
- package/dist/src/cli/cli-flag-specs.js +33 -0
- package/dist/src/cli/cli-query-commands.js +16 -41
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +118 -13
- package/dist/src/cli/cli-start-summary.js +10 -3
- package/dist/src/cli/cli-start.js +33 -5
- package/dist/src/cli/completion.js +21 -1
- package/dist/src/cli/config-commands.js +17 -8
- package/dist/src/cli/help.js +74 -14
- package/dist/src/cli/log-commands.js +26 -13
- package/dist/src/cli/question-commands.js +7 -1
- package/dist/src/cli.js +71 -6
- package/dist/src/collections.js +31 -4
- package/dist/src/config/config-editable-keys.js +3 -0
- package/dist/src/config/config-fallbacks.js +3 -2
- package/dist/src/config/config-validation.js +47 -42
- package/dist/src/config/config-views.js +10 -2
- package/dist/src/config/config-write.js +33 -1
- package/dist/src/config/config.js +7 -2
- package/dist/src/doctor/doctor-backlog-bugs.js +85 -42
- package/dist/src/doctor/doctor-backlog-plans.js +214 -79
- package/dist/src/doctor/doctor-backlog.js +155 -22
- package/dist/src/doctor/doctor-check-names.js +5 -1
- package/dist/src/doctor/doctor-checks.js +10 -15
- package/dist/src/doctor/doctor-claude-checks.js +6 -9
- package/dist/src/doctor/doctor-codex-checks.js +6 -9
- package/dist/src/doctor/doctor-contention-checks.js +244 -118
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +4 -6
- package/dist/src/doctor/doctor-model-checks.js +45 -200
- package/dist/src/doctor/doctor-notify.js +7 -2
- package/dist/src/doctor/doctor-opencode-checks.js +7 -5
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +106 -12
- package/dist/src/doctor/doctor.js +10 -5
- package/dist/src/doctor/probe.js +14 -1
- package/dist/src/events/event-format.js +7 -6
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-read.js +13 -0
- package/dist/src/events/event-window.js +20 -2
- package/dist/src/failure/failure-data.js +9 -4
- package/dist/src/failure/failure-render.js +8 -8
- package/dist/src/failure/time-spend.js +0 -64
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/gates/budget-gates.js +27 -257
- package/dist/src/gates/gate-polls.js +49 -141
- package/dist/src/gates/maintenance-quota.js +27 -17
- package/dist/src/git/commit-message.js +12 -0
- package/dist/src/git/git.js +18 -0
- package/dist/src/git/slot-salvage.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +3 -3
- package/dist/src/gui/gui-backlog-endpoints.js +5 -4
- package/dist/src/gui/gui-command.js +19 -1
- package/dist/src/gui/gui-config-commands.js +75 -9
- package/dist/src/gui/gui-endpoint-commands.js +2 -2
- package/dist/src/gui/gui-operator-commands.js +134 -7
- package/dist/src/gui/gui-prompt-commands.js +15 -6
- package/dist/src/gui/gui-report-endpoints.js +10 -3
- package/dist/src/gui/gui-server.js +16 -2
- package/dist/src/history/history-queued.js +8 -0
- package/dist/src/history/history.js +2 -5
- package/dist/src/inbox/inbox-top.js +4 -9
- package/dist/src/merge/merge-core.js +2 -25
- package/dist/src/merge/merge-drain.js +22 -135
- package/dist/src/merge/merge-pipeline.js +1 -1
- package/dist/src/merge/merge-post-review.js +154 -30
- package/dist/src/merge/merge-queue-render.js +11 -12
- package/dist/src/merge/merge-queue.js +22 -1
- package/dist/src/merge/merge-requeue.js +1 -0
- package/dist/src/merge/merge-stack.js +1 -7
- package/dist/src/merge/merge-to-main.js +1 -9
- package/dist/src/merge/merge-top.js +33 -19
- package/dist/src/merge/merge-verify.js +64 -9
- package/dist/src/notes-cli.js +8 -3
- package/dist/src/orchestrator/orchestrator-poll.js +47 -15
- package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
- package/dist/src/orchestrator/orchestrator.js +5 -4
- package/dist/src/orchestrator/scheduling-scope.js +51 -7
- package/dist/src/orchestrator/scheduling-skip.js +15 -5
- package/dist/src/paths.js +16 -6
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/process/run-marker.js +8 -0
- package/dist/src/prompt/gate-notes.js +1 -1
- package/dist/src/prompt/gate-prompts.js +30 -7
- package/dist/src/prompt/notes-backlog.js +13 -10
- package/dist/src/prompt/prompt.js +3 -1
- package/dist/src/report/report-data.js +49 -69
- package/dist/src/report/report-fold.js +11 -12
- package/dist/src/report/report-render.js +6 -4
- package/dist/src/report/report.js +2 -5
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +12 -0
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +157 -24
- package/dist/src/review/protected-edits.js +22 -7
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review.js +33 -49
- package/dist/src/roles/role-catalog.js +60 -2
- package/dist/src/roles/role-guidance.js +10 -0
- package/dist/src/roles/role-labels.js +6 -3
- package/dist/src/roles/role-render.js +43 -8
- package/dist/src/roles/role-view.js +36 -9
- package/dist/src/roles/roles.js +3 -3
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/qa-coverage.js +50 -15
- package/dist/src/run/run-detail-data.js +9 -13
- package/dist/src/run/run-detail-render.js +1 -8
- package/dist/src/run/run-finalize.js +1 -0
- package/dist/src/run/run-prompt.js +14 -0
- package/dist/src/run/run-stage.js +66 -37
- package/dist/src/run/run-verdict.js +14 -1
- package/dist/src/run/stage-check.js +17 -11
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +16 -515
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +50 -19
- package/dist/src/runner/revision.js +12 -23
- package/dist/src/runner/runner-outcome.js +4 -2
- package/dist/src/runner/runner-recovery.js +18 -3
- package/dist/src/runner/runner-run.js +21 -8
- package/dist/src/runner/runner-state.js +4 -3
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/status/status-data.js +6 -1
- package/dist/src/status/status-polls.js +15 -0
- package/dist/src/status/status-roles.js +11 -0
- package/dist/src/team/reclaim.js +2 -2
- package/dist/src/text/datetime.js +3 -2
- package/dist/src/text/phrases.js +6 -3
- package/dist/src/text/text.js +11 -0
- package/dist/src/ui/badges.js +34 -1
- package/dist/src/ui/gui/gui-client-backlog.js +25 -3
- package/dist/src/ui/gui/gui-client-boot.js +63 -41
- package/dist/src/ui/gui/gui-client-drawer-config.js +56 -14
- package/dist/src/ui/gui/gui-client-drawer.js +29 -4
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +74 -72
- package/dist/src/ui/gui/gui-client-labels.js +5 -1
- package/dist/src/ui/gui/gui-client-operator.js +6 -2
- package/dist/src/ui/gui/gui-client-pending.js +188 -11
- package/dist/src/ui/gui/gui-client-report-chart.js +36 -8
- package/dist/src/ui/gui/gui-client-report.js +20 -5
- package/dist/src/ui/gui/gui-client-settings.js +109 -3
- package/dist/src/ui/gui/gui-client-team.js +11 -0
- package/dist/src/ui/gui/gui-client.js +11 -1
- package/dist/src/ui/gui/gui-page.js +1 -0
- package/dist/src/ui/gui/gui-styles.js +4 -0
- package/dist/src/ui/run-progress-model.js +2 -1
- package/dist/src/ui/status-payload.js +237 -9
- package/dist/src/ui/status-render.js +12 -4
- package/dist/src/ui/team-alerts.js +512 -37
- package/dist/src/ui/tone.js +7 -0
- package/dist/src/ui/tui/tui-backlog.js +7 -5
- package/dist/src/ui/tui/tui-editor-attach.js +1 -7
- package/dist/src/ui/tui/tui-editor-backends.js +1 -6
- package/dist/src/ui/tui/tui-editor-brief.js +4 -24
- package/dist/src/ui/tui/tui-editor-budget.js +1 -8
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
- package/dist/src/ui/tui/tui-editor-config.js +19 -6
- package/dist/src/ui/tui/tui-editor-entry.js +3 -17
- package/dist/src/ui/tui/tui-editor-instances.js +9 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +7 -26
- package/dist/src/ui/tui/tui-editor-interval.js +18 -7
- package/dist/src/ui/tui/tui-editor-notes.js +6 -24
- package/dist/src/ui/tui/tui-editor-quiet.js +2 -12
- package/dist/src/ui/tui/tui-editor-retire.js +1 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +1 -6
- package/dist/src/ui/tui/tui-editor-session.js +41 -0
- package/dist/src/ui/tui/tui-editor-slots.js +1 -0
- package/dist/src/ui/tui/tui-editors.js +10 -1
- package/dist/src/ui/tui/tui-frame.js +18 -6
- package/dist/src/ui/tui/tui-keys.js +9 -0
- package/dist/src/ui/tui/tui-pane-activity.js +21 -8
- package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +113 -3
- package/dist/src/ui/tui/tui-pane-history.js +44 -0
- package/dist/src/ui/tui/tui-pane-pending.js +285 -56
- package/dist/src/ui/tui/tui-pane-queued.js +7 -2
- package/dist/src/ui/tui/tui-pane-report.js +9 -3
- package/dist/src/ui/tui/tui-pane-session.js +9 -0
- package/dist/src/ui/tui/tui-pane.js +16 -10
- package/dist/src/ui/tui/tui-panes.js +56 -7
- package/dist/src/ui/tui/tui.js +8 -5
- package/dist/src/verdict/fix-claim.js +47 -1
- package/dist/src/verdict/reply-contract.js +35 -0
- package/package.json +1 -1
- package/dist/src/baseline/main-culprit.js +0 -94
- package/dist/src/baseline/main-revert.js +0 -133
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ and is also outside `npm test`.
|
|
|
42
42
|
## Status
|
|
43
43
|
|
|
44
44
|
<!-- tumwater:status:start -->
|
|
45
|
-
**v0.
|
|
45
|
+
**v0.4.0**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
|
|
46
46
|
which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
|
|
47
47
|
issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
|
|
48
48
|
|
package/dist/build-info.json
CHANGED
|
@@ -75,3 +75,16 @@ export function modelLabel(backend, provider, model) {
|
|
|
75
75
|
export function backendDisplayName(id) {
|
|
76
76
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
77
77
|
}
|
|
78
|
+
/** A seam's own-default label when neither a provider nor a model resolves:
|
|
79
|
+
* `<backend> default` — pi's historical `pi default` for the pi backend (and an unknown id,
|
|
80
|
+
* which reads as pi), `claude default` for claude, and so on. The one home of the label
|
|
81
|
+
* `role-render.ts`'s Model line and the status rows share, so the two surfaces cannot
|
|
82
|
+
* disagree about what a seam without a model is named. */
|
|
83
|
+
export function backendDefaultLabel(backend) {
|
|
84
|
+
// Only claude, opencode and codex carry their own `<backend> default`; pi, an absent
|
|
85
|
+
// backend and any unregistered id (which reads as pi) all carry pi's historical `pi
|
|
86
|
+
// default`, matching streamsToolOutput's reads-as-pi window.
|
|
87
|
+
if (backend === "claude" || backend === "opencode" || backend === "codex")
|
|
88
|
+
return `${backend} default`;
|
|
89
|
+
return "pi default";
|
|
90
|
+
}
|
|
@@ -20,7 +20,7 @@ import { randomUUID } from "node:crypto";
|
|
|
20
20
|
import { resolvedBackendConfig } from "../../config/config-views.js";
|
|
21
21
|
import { MODEL_TIERS } from "../../config/config-schema.js";
|
|
22
22
|
import { routeModel } from "../../config/model-selector.js";
|
|
23
|
-
import {
|
|
23
|
+
import { backendRunEnv } from "../../process/run-marker.js";
|
|
24
24
|
import { resolveBinForBackend } from "../../pi/pi-bin.js";
|
|
25
25
|
import { expandHomeTilde } from "../../files/files.js";
|
|
26
26
|
import { claudeArgs } from "./claude-args.js";
|
|
@@ -147,7 +147,7 @@ export const claudeBackend = {
|
|
|
147
147
|
// the marker the same way pi's adapter carries it (withNotesPath); from part 2/4 the run
|
|
148
148
|
// saves its note through the `tumwater-notes` command on PATH, so no MCP server reaches the
|
|
149
149
|
// run and `--strict-mcp-config` (always in claude-args.ts) keeps it that way.
|
|
150
|
-
const base =
|
|
150
|
+
const base = backendRunEnv(run);
|
|
151
151
|
const env = claudeEnv(base, {
|
|
152
152
|
quietTimeoutSeconds: run.config.quietTimeoutSeconds,
|
|
153
153
|
...(expandedConfigDir !== undefined ? { configDir: expandedConfigDir } : {}),
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
|
|
16
16
|
* run produced no authoritative `run_cost`. */
|
|
17
17
|
import { joinParts } from "../../text/text.js";
|
|
18
|
+
import { secondsUntil } from "../../text/datetime.js";
|
|
18
19
|
import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
|
|
19
20
|
/** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
|
|
20
21
|
* lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
|
|
@@ -295,8 +296,8 @@ export function claudeTranslator(opts = {}) {
|
|
|
295
296
|
if (info.isUsingOverage === true)
|
|
296
297
|
out.isUsingOverage = true;
|
|
297
298
|
if (status === "rejected" && resetsAt !== undefined) {
|
|
298
|
-
// resetsAt is a unix-seconds stamp;
|
|
299
|
-
out.retryAfterSeconds =
|
|
299
|
+
// resetsAt is a unix-seconds stamp; secondsUntil folds it (×1000) to whole seconds.
|
|
300
|
+
out.retryAfterSeconds = secondsUntil(resetsAt * 1000, Date.now());
|
|
300
301
|
}
|
|
301
302
|
return [JSON.stringify(out), ...windows];
|
|
302
303
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import { loadConfigSafe } from "../../config/config.js";
|
|
21
21
|
import { resolvedBackendConfig } from "../../config/config-views.js";
|
|
22
|
-
import {
|
|
22
|
+
import { backendRunEnv } from "../../process/run-marker.js";
|
|
23
23
|
import { resolveBinForBackend } from "../../pi/pi-bin.js";
|
|
24
24
|
import { teamRootOf, nearestTumwaterRoot } from "../../paths.js";
|
|
25
25
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../../events/events.js";
|
|
@@ -120,7 +120,7 @@ export const codexBackend = {
|
|
|
120
120
|
// The notes path rides beside the run marker, as the pi and claude adapters' envs do
|
|
121
121
|
// (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes`
|
|
122
122
|
// command on PATH, so codex configures no MCP server for notes.
|
|
123
|
-
const base =
|
|
123
|
+
const base = backendRunEnv(run);
|
|
124
124
|
// A team home of its own (`CODEX_HOME`) keeps team sessions and trust entries out of the
|
|
125
125
|
// operator's `~/.codex` (design §4, finding 10); unset leaves the operator's home.
|
|
126
126
|
const home = codexHome(resolvedBackendConfig(run.config, "codex"));
|
|
@@ -162,7 +162,7 @@ export const codexBackend = {
|
|
|
162
162
|
// invocation later reads was looked up under the run's own config. It never throws a run
|
|
163
163
|
// away: a failed lookup records `off` (and warns once) and the run continues without the
|
|
164
164
|
// guard's trust.
|
|
165
|
-
const base =
|
|
165
|
+
const base = backendRunEnv(run);
|
|
166
166
|
const home = codexHome(resolvedBackendConfig(run.config, "codex"));
|
|
167
167
|
const env = codexEnv(base, { home });
|
|
168
168
|
const root = nearestTumwaterRoot(run.sessionDir);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
17
17
|
import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
|
|
18
|
+
import { secondsUntil } from "../../text/datetime.js";
|
|
18
19
|
/** How codex wraps every shell command — the prefix to strip (with its quoting) so the
|
|
19
20
|
* canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
|
|
20
21
|
const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
|
|
@@ -76,7 +77,8 @@ function classifyTurnFailed(parsed) {
|
|
|
76
77
|
const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
|
|
77
78
|
const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
|
|
78
79
|
const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
|
|
79
|
-
|
|
80
|
+
// secondsUntil folds the unix-seconds resets stamp (×1000 to epoch-ms), clamped at 0.
|
|
81
|
+
const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
|
|
80
82
|
kind.kind = "transientRateLimit";
|
|
81
83
|
kind.rateLimit = {
|
|
82
84
|
type: "rate_limit",
|
|
@@ -23,7 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import { ensureParentDir } from "../../files/files.js";
|
|
24
24
|
import { loadConfigSafe } from "../../config/config.js";
|
|
25
25
|
import { resolvedBackendConfig } from "../../config/config-views.js";
|
|
26
|
-
import {
|
|
26
|
+
import { backendRunEnv } from "../../process/run-marker.js";
|
|
27
27
|
import { resolveBinForBackend } from "../../pi/pi-bin.js";
|
|
28
28
|
import { opencodeDbPath, teamRootOf } from "../../paths.js";
|
|
29
29
|
import { resumableSession } from "../session-ledger.js";
|
|
@@ -189,7 +189,7 @@ export const opencodeBackend = {
|
|
|
189
189
|
// The notes path rides beside the run marker, as the pi and claude adapters' envs do
|
|
190
190
|
// (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes` command
|
|
191
191
|
// on PATH, so OpenCode configures no tumwater MCP server for notes.
|
|
192
|
-
const base =
|
|
192
|
+
const base = backendRunEnv(run);
|
|
193
193
|
const env = opencodeEnv(base, { dbPath, configContent });
|
|
194
194
|
const { sessionId } = resolveOpencodeSession(run, env);
|
|
195
195
|
// The thinking variant (part 6d/7): ride the catalog's effort levels for the resolved
|
|
@@ -412,14 +412,16 @@ function quarantineTitlePrefix(test) {
|
|
|
412
412
|
function fencedTail(tail) {
|
|
413
413
|
return ["~~~~", ...tail.replaceAll("~~~~", "~~~").split("\n").slice(-30), "~~~~"].join("\n");
|
|
414
414
|
}
|
|
415
|
-
/** File one BUGS.md `## Open` entry per
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
|
|
415
|
+
/** File one BUGS.md `## Open` entry per given entry and commit BUGS.md on the base branch as one
|
|
416
|
+
* `tumwater(operator):` commit (the subject built from the number of entries actually appended is
|
|
417
|
+
* stamped under the operator role), under the merge lock and the primary-worktree lock exactly as
|
|
418
|
+
* archiveFinishedOnMain does. Skipped,
|
|
419
|
+
* writing nothing, when the primary checkout is off the base branch, BUGS.md has uncommitted
|
|
420
|
+
* changes, or BUGS.md exists but cannot be read; a failed append or commit restores BUGS.md.
|
|
421
|
+
* Every entry is appended verbatim as its own `## Open` heading — unless its `ifOpen` dedupe
|
|
422
|
+
* matches an existing heading, which is reused instead — and the returned headings are the
|
|
423
|
+
* entry headings in input order. */
|
|
424
|
+
export async function fileBugsOnMain(root, entries, subject, opts = {}) {
|
|
423
425
|
return withLock(mergeLockDir(root), () => withPrimaryWorktreeLockAsync(root, async () => {
|
|
424
426
|
const primary = await currentBranch(root);
|
|
425
427
|
const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
|
|
@@ -440,9 +442,8 @@ export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
|
|
|
440
442
|
catch (err) {
|
|
441
443
|
return { status: "skipped", reason: `BUGS.md could not be read: ${errorFirstLine(err)}` };
|
|
442
444
|
}
|
|
443
|
-
const date = formatDate(new Date());
|
|
444
445
|
const open = before === null ? [] : parseEntryDetails(before, "Open").map((e) => e.title);
|
|
445
|
-
const
|
|
446
|
+
const headings = [];
|
|
446
447
|
let appended = 0;
|
|
447
448
|
const restoreBugs = async () => {
|
|
448
449
|
await gitTry(root, "reset", "-q", "--", "BUGS.md");
|
|
@@ -452,17 +453,15 @@ export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
|
|
|
452
453
|
writeTextAtomic(file, before);
|
|
453
454
|
};
|
|
454
455
|
try {
|
|
455
|
-
for (const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
456
|
+
for (const entry of entries) {
|
|
457
|
+
if (entry.ifOpen !== undefined) {
|
|
458
|
+
const existing = entry.ifOpen(open);
|
|
459
|
+
if (existing !== undefined) {
|
|
460
|
+
headings.push(existing);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
460
463
|
}
|
|
461
|
-
|
|
462
|
-
`The check failed on this test and then passed on a re-run of the same tree, so tumwater quarantined it: it counts as a known failure until this entry leaves \`## Open\`. Find why it is unstable and fix it.`,
|
|
463
|
-
...(tail ? ["", fencedTail(tail)] : []),
|
|
464
|
-
].join("\n");
|
|
465
|
-
bugs.set(test, appendBugEntryLocked(root, `${quarantineTitlePrefix(test)}${date})`, body));
|
|
464
|
+
headings.push(appendBugEntryLocked(root, entry.title, entry.body));
|
|
466
465
|
appended++;
|
|
467
466
|
}
|
|
468
467
|
}
|
|
@@ -471,17 +470,53 @@ export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
|
|
|
471
470
|
return { status: "skipped", reason: `a BUGS.md entry could not be written: ${errorFirstLine(err)}` };
|
|
472
471
|
}
|
|
473
472
|
if (appended === 0)
|
|
474
|
-
return { status: "filed",
|
|
475
|
-
const
|
|
476
|
-
|
|
473
|
+
return { status: "filed", headings };
|
|
474
|
+
const commitSubject = stampedSubject("operator", // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
475
|
+
subject(appended));
|
|
477
476
|
try {
|
|
478
477
|
await git(root, "add", "--", "BUGS.md");
|
|
479
|
-
await git(root, ...(await committerIdentArgs(root)), "commit", ...commitAuthorArgs(), "--only", "-m",
|
|
478
|
+
await git(root, ...(await committerIdentArgs(root)), "commit", ...commitAuthorArgs(), "--only", "-m", commitSubject, "--", "BUGS.md");
|
|
480
479
|
}
|
|
481
480
|
catch (err) {
|
|
482
481
|
await restoreBugs();
|
|
483
482
|
return { status: "skipped", reason: `the commit failed: ${errorFirstLine(err)}` };
|
|
484
483
|
}
|
|
485
|
-
return { status: "filed",
|
|
484
|
+
return { status: "filed", headings };
|
|
486
485
|
}), opts.lockTimeoutMs);
|
|
487
486
|
}
|
|
487
|
+
/** File one BUGS.md `## Open` entry per newly quarantined test and commit BUGS.md on the base
|
|
488
|
+
* branch as one `tumwater(operator):` commit, delegating the locked write/commit to
|
|
489
|
+
* fileBugsOnMain and preserving the existing subject. A test whose quarantine entry is already
|
|
490
|
+
* under `## Open` (a filing whose markQuarantineFiled failed after the commit) gets that heading
|
|
491
|
+
* back and is not filed again — the dedupe runs inside fileBugsOnMain under the merge lock (via
|
|
492
|
+
* each entry's `ifOpen`), so concurrent quarantine filings cannot both append a duplicate, and
|
|
493
|
+
* the real skip on a dirty or off-base BUGS.md is enforced under the same lock. */
|
|
494
|
+
export async function fileQuarantineBugsOnMain(root, tests, opts = {}) {
|
|
495
|
+
const date = formatDate(new Date());
|
|
496
|
+
const toFile = [];
|
|
497
|
+
for (const { test, tail } of tests) {
|
|
498
|
+
const prefix = quarantineTitlePrefix(test);
|
|
499
|
+
const body = [
|
|
500
|
+
`The check failed on this test and then passed on a re-run of the same tree, so tumwater quarantined it: it counts as a known failure until this entry leaves \`## Open\`. Find why it is unstable and fix it.`,
|
|
501
|
+
...(tail ? ["", fencedTail(tail)] : []),
|
|
502
|
+
].join("\n");
|
|
503
|
+
toFile.push({
|
|
504
|
+
title: `${prefix}${date})`,
|
|
505
|
+
body,
|
|
506
|
+
// Dedupe under the lock, against the headings fileBugsOnMain reads: the reused heading
|
|
507
|
+
// (not a duplicate) is returned in headings and zipped back to this test below.
|
|
508
|
+
ifOpen: (open) => open.find((t) => t.startsWith(prefix)),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const outcome = await fileBugsOnMain(root, toFile,
|
|
512
|
+
// Size the subject by what was actually appended: tests whose `## Open` entry was already
|
|
513
|
+
// present are reused, not filed, so a mixed batch keeps the original "File N …" wording
|
|
514
|
+
// with N the count of new entries only.
|
|
515
|
+
(appended) => `File ${plural(appended, "quarantined flaky test", "quarantined flaky tests")} in BUGS.md`, opts);
|
|
516
|
+
if (outcome.status === "skipped")
|
|
517
|
+
return { status: "skipped", reason: outcome.reason };
|
|
518
|
+
// fileBugsOnMain's headings align with `tests` in order (reused headings or appended titles).
|
|
519
|
+
const bugs = new Map();
|
|
520
|
+
outcome.headings.forEach((heading, i) => bugs.set(tests[i].test, heading));
|
|
521
|
+
return { status: "filed", bugs };
|
|
522
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* shells (file, cancel, release, top, edit). Neither shell gates on a ready repo, commits, or
|
|
4
4
|
* wakes a role. cli.ts reaches them through listAndAnnounce and showAndAnnounce. */
|
|
5
5
|
import { fail, say, sayJson } from "../cli/cli-output.js";
|
|
6
|
-
import { parseCountFlag,
|
|
6
|
+
import { parseCountFlag, rejectRepeatFlag, scanPositionArgs } from "../cli/cli-args.js";
|
|
7
7
|
import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, donePlanEntries, fixedBugEntries, openBugEntries, plannedPlanEntries, ageInDays, entryAgeSuffix, entryMatches, filingDate, } from "./backlog.js";
|
|
8
8
|
import { backlogHolds, backlogInProgress, holdMark, inProgressMark } from "./backlog-eligibility.js";
|
|
9
9
|
import { groupBlockedSeries, partsLabel } from "./backlog-structure.js";
|
|
@@ -221,37 +221,12 @@ export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
|
|
|
221
221
|
* `--list`: no repo gate, commit, or wake. Prose is the title, a blank line, then the body, so
|
|
222
222
|
* stdout is the file `--edit <n> --file` takes back. */
|
|
223
223
|
export function showAndAnnounce(root, args, command) {
|
|
224
|
-
|
|
225
|
-
let json = false;
|
|
226
|
-
let done = false;
|
|
227
|
-
const rest = [];
|
|
228
|
-
for (let i = 0; i < args.length; i++) {
|
|
229
|
-
const arg = args[i];
|
|
230
|
-
if (arg === "--done") {
|
|
231
|
-
rejectRepeatFlag(done, "--done");
|
|
232
|
-
done = true;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (arg === "--json") {
|
|
236
|
-
rejectRepeatFlag(json, "--json");
|
|
237
|
-
json = true;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
if (arg === "--show") {
|
|
241
|
-
rejectRepeatFlag(position !== null, "--show");
|
|
242
|
-
position = parsePositionFlag("--show", args[i + 1], "--show 2");
|
|
243
|
-
i++;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (arg === "--help" || arg === "-h")
|
|
247
|
-
continue;
|
|
248
|
-
rest.push(arg);
|
|
249
|
-
}
|
|
224
|
+
const { json, flags, position, rest } = scanPositionArgs(args, "--show", "--show 2", ["--done"]);
|
|
250
225
|
rejectUnknownArgs(command, rest, []);
|
|
251
226
|
refuseTeamWorkspace(root);
|
|
252
227
|
if (position === null)
|
|
253
228
|
fail(`--show needs a position (usage: tumwater ${command} --show <n>)`);
|
|
254
|
-
if (done) {
|
|
229
|
+
if (flags["--done"]) {
|
|
255
230
|
const bug = command === "bug";
|
|
256
231
|
const all = bug ? fixedBugEntries(root) : donePlanEntries(root);
|
|
257
232
|
const entry = all[position - 1];
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* surgery and the locked read-modify-write live in backlog-write.ts. The wake of the role that
|
|
7
7
|
* should act stays in cli.ts beside the other command bodies. */
|
|
8
8
|
import { fail, say, sayJson, warnIfUncommitted } from "../cli/cli-output.js";
|
|
9
|
-
import { parsePositionFlag, rejectRepeatFlag } from "../cli/cli-args.js";
|
|
9
|
+
import { parsePositionFlag, rejectRepeatFlag, scanPositionArgs } from "../cli/cli-args.js";
|
|
10
10
|
import { promptFileValue, readPromptFileArg } from "../cli/cli-command-args.js";
|
|
11
11
|
import { plannedPlanEntries } from "./backlog.js";
|
|
12
12
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
13
|
import { backlogHolds, cancelledClaimNote, stillHeldNote } from "./backlog-eligibility.js";
|
|
14
14
|
import { entryKey, plansReleasedBy, releasedPartsNote } from "./backlog-parts.js";
|
|
15
|
+
import { liftedNotePhrase } from "./backlog-release.js";
|
|
15
16
|
import { collapseWhitespace } from "../text/text.js";
|
|
16
17
|
import { JSON_FLAG, rejectUnknownArgs } from "../cli/cli-flag-specs.js";
|
|
17
18
|
import { requireReadyRepo } from "../cli/cli-repo-gate.js";
|
|
@@ -148,26 +149,7 @@ async function announceFiled(root, command, filed, role, wakeText, json) {
|
|
|
148
149
|
* `cancelled <command> in <file>: <title>` or the `--json` payload {file, title}. Unlike
|
|
149
150
|
* fileAndAnnounce there is no wake: removing work must not wake a role. */
|
|
150
151
|
export async function cancelAndAnnounce(root, args, command, cancel) {
|
|
151
|
-
|
|
152
|
-
let json = false;
|
|
153
|
-
const rest = [];
|
|
154
|
-
for (let i = 0; i < args.length; i++) {
|
|
155
|
-
const arg = args[i];
|
|
156
|
-
if (arg === "--json") {
|
|
157
|
-
rejectRepeatFlag(json, "--json");
|
|
158
|
-
json = true;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (arg === "--cancel") {
|
|
162
|
-
rejectRepeatFlag(position !== null, "--cancel");
|
|
163
|
-
position = parsePositionFlag("--cancel", args[i + 1], "--cancel 2");
|
|
164
|
-
i++;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (arg === "--help" || arg === "-h")
|
|
168
|
-
continue;
|
|
169
|
-
rest.push(arg);
|
|
170
|
-
}
|
|
152
|
+
const { json, position, rest } = scanPositionArgs(args, "--cancel", "--cancel 2");
|
|
171
153
|
// rejectUnknownArgs names both a stray positional and an unknown flag, with this command's
|
|
172
154
|
// (empty) valid-flag list — the vocabulary a cancel invocation accepts is exactly --cancel
|
|
173
155
|
// and --json, both consumed above.
|
|
@@ -197,28 +179,10 @@ export async function cancelAndAnnounce(root, args, command, cancel) {
|
|
|
197
179
|
* released entry is eligible now, and that role may be backed off) with no queued prompt. An
|
|
198
180
|
* entry still held after the release (another note, an unmerged prerequisite) wakes no one and
|
|
199
181
|
* gets stillHeldNote instead of the wake line. Prints `released <command> in <file>: <title>`
|
|
200
|
-
* plus that line, or only the `--json` payload (with `
|
|
182
|
+
* naming the note it lifted, plus that line, or only the `--json` payload (with `lifted`, and
|
|
183
|
+
* `hold` when still held). */
|
|
201
184
|
export async function releaseAndAnnounce(root, args, command, release, role) {
|
|
202
|
-
|
|
203
|
-
let json = false;
|
|
204
|
-
const rest = [];
|
|
205
|
-
for (let i = 0; i < args.length; i++) {
|
|
206
|
-
const arg = args[i];
|
|
207
|
-
if (arg === "--json") {
|
|
208
|
-
rejectRepeatFlag(json, "--json");
|
|
209
|
-
json = true;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (arg === "--release") {
|
|
213
|
-
rejectRepeatFlag(position !== null, "--release");
|
|
214
|
-
position = parsePositionFlag("--release", args[i + 1], "--release 2");
|
|
215
|
-
i++;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (arg === "--help" || arg === "-h")
|
|
219
|
-
continue;
|
|
220
|
-
rest.push(arg);
|
|
221
|
-
}
|
|
185
|
+
const { json, position, rest } = scanPositionArgs(args, "--release", "--release 2");
|
|
222
186
|
rejectUnknownArgs(command, rest, []);
|
|
223
187
|
if (position === null)
|
|
224
188
|
fail(`--release needs a position (usage: tumwater ${command} --release <n>)`);
|
|
@@ -229,9 +193,9 @@ export async function releaseAndAnnounce(root, args, command, release, role) {
|
|
|
229
193
|
const hold = (command === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
|
|
230
194
|
const wake = hold === null ? requestWake(root, [role]) : "";
|
|
231
195
|
if (json)
|
|
232
|
-
sayJson({ file: released.file, title: released.title, ...(hold === null ? {} : { hold }) });
|
|
196
|
+
sayJson({ file: released.file, title: released.title, lifted: released.lifted, ...(hold === null ? {} : { hold }) });
|
|
233
197
|
else {
|
|
234
|
-
say(`released ${command} in ${released.file}: ${released.title}`);
|
|
198
|
+
say(`released ${command} in ${released.file}: ${released.title} (lifted ${liftedNotePhrase(released.lifted)})`);
|
|
235
199
|
say(hold === null ? wake : stillHeldNote(hold));
|
|
236
200
|
}
|
|
237
201
|
}
|
|
@@ -242,26 +206,7 @@ export async function releaseAndAnnounce(root, args, command, release, role) {
|
|
|
242
206
|
* (then stillHeldNote when the entry is held) or only the `--json` payload (with `hold` when
|
|
243
207
|
* held). */
|
|
244
208
|
export async function topAndAnnounce(root, args, command, top) {
|
|
245
|
-
|
|
246
|
-
let json = false;
|
|
247
|
-
const rest = [];
|
|
248
|
-
for (let i = 0; i < args.length; i++) {
|
|
249
|
-
const arg = args[i];
|
|
250
|
-
if (arg === "--json") {
|
|
251
|
-
rejectRepeatFlag(json, "--json");
|
|
252
|
-
json = true;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (arg === "--top") {
|
|
256
|
-
rejectRepeatFlag(position !== null, "--top");
|
|
257
|
-
position = parsePositionFlag("--top", args[i + 1], "--top 2");
|
|
258
|
-
i++;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (arg === "--help" || arg === "-h")
|
|
262
|
-
continue;
|
|
263
|
-
rest.push(arg);
|
|
264
|
-
}
|
|
209
|
+
const { json, position, rest } = scanPositionArgs(args, "--top", "--top 2");
|
|
265
210
|
rejectUnknownArgs(command, rest, []);
|
|
266
211
|
if (position === null)
|
|
267
212
|
fail(`--top needs a position (usage: tumwater ${command} --top <n>)`);
|
|
@@ -5,10 +5,12 @@ import { commitAuthorArgs, committerIdentArgs, gitTry } from "../git/git-run.js"
|
|
|
5
5
|
/** Commit one operator-written backlog file on main, so the entry reaches the worktrees that
|
|
6
6
|
* branch from main and the primary checkout stays clean for the next fast-forward. Runs under
|
|
7
7
|
* the primary-worktree lock `ffMainTo`'s working-tree arm takes, so the commit and a
|
|
8
|
-
* fast-forward cannot interleave. Returns false
|
|
9
|
-
* the base branch (the ref-push arm: the working tree is not main's) or
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* fast-forward cannot interleave. Returns false when the commit did not land — the primary is
|
|
9
|
+
* not on the base branch (the ref-push arm: the working tree is not main's), or git failed —
|
|
10
|
+
* and in that case restores the file to its committed state so the primary checkout does not
|
|
11
|
+
* stay dirty and block every merge touching it with "the merge would overwrite files in the
|
|
12
|
+
* primary checkout". It never throws. `--only` with a pathspec commits just that file's
|
|
13
|
+
* working-tree content and leaves other staged or dirty paths alone. */
|
|
12
14
|
export async function commitOperatorBacklogEdit(root, file, subject) {
|
|
13
15
|
try {
|
|
14
16
|
// Yield first: a synchronous caller (the TUI key handler) may write again right after this
|
|
@@ -17,11 +19,17 @@ export async function commitOperatorBacklogEdit(root, file, subject) {
|
|
|
17
19
|
await new Promise((resolve) => setImmediate(resolve));
|
|
18
20
|
return await withPrimaryWorktreeLockAsync(root, async () => {
|
|
19
21
|
const primary = await currentBranch(root);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
let committed = primary !== null &&
|
|
23
|
+
targetBranch(loadConfigSafe(root).config?.baseBranch, primary) === primary &&
|
|
24
|
+
(await gitTry(root, ...(await committerIdentArgs(root)), "commit", ...commitAuthorArgs(), "--only", "-m", subject, "--", file)) !== null;
|
|
25
|
+
if (!committed) {
|
|
26
|
+
// The write landed in the working tree but could not be committed to the base branch:
|
|
27
|
+
// roll the file back to its committed state (HEAD) so the primary checkout stays clean
|
|
28
|
+
// for the next fast-forward instead of refusing every merge that touches it. Only this
|
|
29
|
+
// file is touched; other staged or dirty paths are left alone.
|
|
30
|
+
await gitTry(root, "checkout", "HEAD", "--", file);
|
|
31
|
+
}
|
|
32
|
+
return committed;
|
|
25
33
|
});
|
|
26
34
|
}
|
|
27
35
|
catch {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { statOrNull } from "../files/files.js";
|
|
12
12
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
|
-
import { firstTrimmedLineStartingWith } from "../text/text.js";
|
|
13
|
+
import { escapeRegExp, firstTrimmedLineStartingWith } from "../text/text.js";
|
|
14
14
|
import { HELD_TAG_PREFIX, NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX, PRIOR_HELD_PREFIX, WAITING_FOR_FAILURE_PREFIX } from "../roles/role-guidance.js";
|
|
15
15
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
16
|
import { roleLabel } from "../roles/role-labels.js";
|
|
@@ -25,7 +25,7 @@ export const OPERATOR_HOLD_THRESHOLD = 3;
|
|
|
25
25
|
* but non-numeric `(held …)` tag is 0 (and stops the scan), so a malformed tag never throws.
|
|
26
26
|
* Pure over the body text — the data-layer read half of the convergence backstop. */
|
|
27
27
|
export function heldCount(body) {
|
|
28
|
-
const tagRe = new RegExp(`${HELD_TAG_PREFIX
|
|
28
|
+
const tagRe = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`);
|
|
29
29
|
for (const raw of body.split("\n")) {
|
|
30
30
|
const line = raw.trimStart();
|
|
31
31
|
const note = line.startsWith(NEEDS_REVIEW_PREFIX) || line.startsWith(NEEDS_REPLAN_PREFIX);
|
|
@@ -55,7 +55,7 @@ function lineHeldCount(line) {
|
|
|
55
55
|
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX) || trimmed.startsWith(NEEDS_REPLAN_PREFIX);
|
|
56
56
|
if (!isNote)
|
|
57
57
|
return undefined;
|
|
58
|
-
const tag = new RegExp(`${HELD_TAG_PREFIX
|
|
58
|
+
const tag = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`).exec(trimmed);
|
|
59
59
|
return tag === null ? undefined : Number(tag[1]);
|
|
60
60
|
}
|
|
61
61
|
/** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
|
|
@@ -331,37 +331,55 @@ export function backlogMarks(root) {
|
|
|
331
331
|
bugs: holds.bugs.map((h, i) => mark(h, working.bugs[i])),
|
|
332
332
|
};
|
|
333
333
|
}
|
|
334
|
-
/**
|
|
335
|
-
* until the operator lifts them with `tumwater plan --release`
|
|
336
|
-
* alert's input), each carrying the backlog file
|
|
337
|
-
* opening the row can land on it, the same RefusedEntry
|
|
338
|
-
*
|
|
334
|
+
/** A planned plan or open bug whose hold is `"held-for-operator"`, in file order: the
|
|
335
|
+
* entries parked until the operator lifts them with `tumwater plan --release` or `tumwater
|
|
336
|
+
* bug --release` (the `operator-held` team alert's input), each carrying the backlog file
|
|
337
|
+
* and its 0-based position so a surface opening the row can land on it, the same RefusedEntry
|
|
338
|
+
* shape refusedEntries returns. The convergence hold writes only on plans, but a role's own
|
|
339
|
+
* too-large note can sit on a bug and reach this threshold, so a bug can be held this way too.
|
|
339
340
|
* A missing file yields `[]`. */
|
|
340
341
|
export function operatorHeldEntries(root) {
|
|
341
342
|
const holds = backlogHolds(root);
|
|
342
343
|
const out = [];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
const entriesOf = (entries, held, file) => {
|
|
345
|
+
entries.forEach((e, i) => {
|
|
346
|
+
if (held[i] === "held-for-operator")
|
|
347
|
+
out.push({ file, index: i, title: e.title }); // vocabulary-ok: the held-for-operator hold's key
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
351
|
+
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
347
352
|
return out;
|
|
348
353
|
}
|
|
349
|
-
/** The
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
/** The display noun for a backlog-file discriminator — the `"plans" | "bugs"` `file` on
|
|
355
|
+
* RefusedEntry and StalledEntry (and the same discriminator the prompt's named-hold wording
|
|
356
|
+
* and the doctor's Stalled row take): BUGS.md is a "Bug" and PLANS.md a "Plan", and the
|
|
357
|
+
* lowercase form is the `Refused plan|bug:` label and the `tumwater plan|bug --release`
|
|
358
|
+
* command noun the operator-held alert body interpolates. */
|
|
359
|
+
export function entryKindLabel(file) {
|
|
360
|
+
return file === "bugs" ? "Bug" : "Plan";
|
|
361
|
+
}
|
|
362
|
+
/** The planned plans and open bugs whose hold is exactly `"needs-review"` or
|
|
363
|
+
* `"needs-replan"`, in file order: the entries parked by a Needs-review/Needs-replan note
|
|
364
|
+
* until the plan role splits or replans them (the `stalled-entries` team alert's input), each
|
|
365
|
+
* carrying its backlog file, 0-based index and the hold. Same `backlogHolds(root)` + the
|
|
366
|
+
* planned/`openBugEntries` readers `operatorHeldEntries` uses, so the alert and the doctor's
|
|
367
|
+
* `stalled entries` row classify through the same `entryHold` and cannot disagree on which
|
|
368
|
+
* holds are role-resolvable. A held-for-operator entry (a note at the held threshold) is
|
|
369
|
+
* excluded — it feeds `operatorHeldEntries` and the `operator-held` alert instead. A missing
|
|
370
|
+
* file yields `[]`. */
|
|
371
|
+
export function stalledEntries(root) {
|
|
358
372
|
const holds = backlogHolds(root);
|
|
359
373
|
const out = [];
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
374
|
+
const entriesOf = (entries, held, file) => {
|
|
375
|
+
entries.forEach((e, i) => {
|
|
376
|
+
const hold = held[i];
|
|
377
|
+
if (hold === "needs-review" || hold === "needs-replan")
|
|
378
|
+
out.push({ file, index: i, title: e.title, hold });
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
382
|
+
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
365
383
|
return out;
|
|
366
384
|
}
|
|
367
385
|
/** The planned plans, then the open bugs, whose hold is `"refused"`, in file order: the
|