tumwater 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/** The seam's answer to "who in this team can read images" (plans/image-input.md
|
|
2
|
-
* "Capability"). One resolver, `seamImageInput`, is the only place that rule lives
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* seam
|
|
6
|
-
* declares a hook. */
|
|
7
|
-
import { configForRole } from "../config/config-views.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
* "Capability"). One resolver, `seamImageInput`, is the only place that rule lives; the reroute
|
|
3
|
+
* target (imageReroute) and the per-role surfaces (roleImageInfo) both read it, so the answer
|
|
4
|
+
* is never reached two ways. It lives at runtime level because it needs the backend registry:
|
|
5
|
+
* a claude seam is always capable through the AgentBackend.imageInput hook, a pi seam reads the
|
|
6
|
+
* model's `input` in models.json, and any other backend answers null unless it declares a hook. */
|
|
7
|
+
import { configForRole, roleSeamTier, withModelOverrides } from "../config/config-views.js";
|
|
8
|
+
import { BORROW_ORDER, resolveTierFallbacks } from "../config/config-fallbacks.js";
|
|
9
|
+
import { seamAgent } from "../config/config-model-selector.js";
|
|
10
|
+
import { modelLabel } from "./backend-traits.js";
|
|
11
|
+
import { piModelImageInput, piPairFree, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
10
12
|
import { piListedImageInput } from "../pi/pi-listed-models.js";
|
|
11
13
|
import { backendById } from "./agent-backend.js";
|
|
12
14
|
/** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
|
|
@@ -32,12 +34,67 @@ export function seamImageInput(seam, providers, listed = null) {
|
|
|
32
34
|
return null;
|
|
33
35
|
return piModelImageInput(providers, seam.provider, seam.model) ?? piListedImageInput(listed, seam.provider, seam.model);
|
|
34
36
|
}
|
|
37
|
+
/** The image-capable reroute target for `role`, or null when no peer tier can read images
|
|
38
|
+
* (plans/image-input.md "Reroute target"). Walks the role's seam tier through BORROW_ORDER,
|
|
39
|
+
* skipping the tier itself — small → default → strong, default → strong → small, strong →
|
|
40
|
+
* default (a strong-tier seam never drops to small, the same rule as the budget fallback) —
|
|
41
|
+
* and takes the first tier whose model reads images. `regime` picks the pair set (see
|
|
42
|
+
* RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
|
|
43
|
+
* or account. */
|
|
44
|
+
export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, providers = null, listed = null) {
|
|
45
|
+
const tier = roleSeamTier(config, role);
|
|
46
|
+
const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
|
|
47
|
+
if (candidates.length === 0)
|
|
48
|
+
return null;
|
|
49
|
+
const providerMap = providers ?? readPiProviders(modelsPath);
|
|
50
|
+
const capable = (rc) => seamImageInput(rc, providerMap, listed) === true;
|
|
51
|
+
if (regime === "primary") {
|
|
52
|
+
for (const t of candidates) {
|
|
53
|
+
const rc = withModelOverrides(config, t, {});
|
|
54
|
+
if (capable(rc))
|
|
55
|
+
return { tier: t, config: rc };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
|
|
60
|
+
// price check), never a priced primary model — a reroute must not spend past the cap.
|
|
61
|
+
const resolved = resolveTierFallbacks(config, (p) =>
|
|
62
|
+
// The free-ness lookup reads the one claude account held here (part 2a/9).
|
|
63
|
+
piPairFree(providerMap, p, (b) => (b === "claude" ? account : null)));
|
|
64
|
+
for (const t of candidates) {
|
|
65
|
+
const pair = resolved[t].pair;
|
|
66
|
+
if (!pair)
|
|
67
|
+
continue;
|
|
68
|
+
const rc = withModelOverrides(config, t, {
|
|
69
|
+
provider: pair.provider,
|
|
70
|
+
model: pair.model,
|
|
71
|
+
thinking: pair.thinking,
|
|
72
|
+
});
|
|
73
|
+
if (capable(rc))
|
|
74
|
+
return { tier: t, config: rc };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** The config of imageReroute, for callers that want only the target model. */
|
|
79
|
+
export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, providers = null, listed = null) {
|
|
80
|
+
return imageReroute(config, role, regime, modelsPath, account, providers, listed)?.config ?? null;
|
|
81
|
+
}
|
|
82
|
+
/** A seam's model as one readable token, the same label the doctor's `image input` check used
|
|
83
|
+
* (`claude/<id>` for a claude seam, `provider/model` for pi), delegated to backend-traits'
|
|
84
|
+
* modelLabel. Used by roleImageInfo alone. */
|
|
85
|
+
function seamImageLabel(backend, provider, model) {
|
|
86
|
+
return modelLabel(backend, provider, model);
|
|
87
|
+
}
|
|
35
88
|
/** Compute RoleImageInfo for one role. `modelsPath` and `listed` are injectable; the providers
|
|
36
|
-
* catalog is read (stat-cached) here
|
|
37
|
-
*
|
|
89
|
+
* catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
|
|
90
|
+
* roles still parses the catalog once. */
|
|
38
91
|
export function roleImageInfo(config, role, modelsPath = piModelsPath(), listed = null) {
|
|
39
92
|
const providers = readPiProviders(modelsPath);
|
|
40
93
|
const rc = configForRole(config, role);
|
|
41
94
|
const imageInput = seamImageInput(rc, providers, listed);
|
|
42
|
-
|
|
95
|
+
const rr = imageReroute(config, role, "primary", modelsPath, null, providers, listed);
|
|
96
|
+
const imageRerouteStr = rr
|
|
97
|
+
? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
|
|
98
|
+
: null;
|
|
99
|
+
return { imageInput, imageReroute: imageRerouteStr };
|
|
43
100
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
|
|
2
|
-
* (plans/notes-command.md "Reaching it from a run"):
|
|
2
|
+
* (plans/notes-command.md "Reaching it from a run"): agent-spawn calls `ensureNotesShim(root)`
|
|
3
3
|
* before building each invocation, and the backend prepends the returned dir to the run's
|
|
4
4
|
* PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
|
|
5
5
|
* script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
|
|
@@ -20,7 +20,7 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
|
|
|
20
20
|
/** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
|
|
21
21
|
* The shim is written atomically with mode 0755, only when its content differs from what is
|
|
22
22
|
* already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
|
|
23
|
-
* write is the caller's to absorb —
|
|
23
|
+
* write is the caller's to absorb — agent-spawn logs one warning and runs without the shim. */
|
|
24
24
|
export function ensureNotesShim(root, execPath = process.execPath) {
|
|
25
25
|
const dir = notesShimDir(root);
|
|
26
26
|
const target = path.join(dir, NOTES_SHIM_NAME);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
|
|
2
2
|
* "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
|
|
3
3
|
* json` into the canonical stream, runs the authoritative session export after the process
|
|
4
|
-
* exits, and classifies the exit's stderr — everything backend-specific. src/agent/
|
|
4
|
+
* exits, and classifies the exit's stderr — everything backend-specific. src/agent/agent-spawn.ts
|
|
5
5
|
* spawns the run and records this run in the session ledger; opencode mints its own session id
|
|
6
6
|
* (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
|
|
7
7
|
* carry it.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* the only thing that stops a follow-up running against an empty session. Anything else mints a
|
|
14
14
|
* fresh id (design §4).
|
|
15
15
|
*
|
|
16
|
-
* Registration lives in src/agent/run
|
|
16
|
+
* Registration lives in src/agent/agent-run.ts beside the pi and claude adapters; a test selects
|
|
17
17
|
* opencode by setting `config.agent = "opencode"` on a run. A run carrying a note gets the
|
|
18
18
|
* notes path in its environment (the `tumwater-notes` command on PATH reads it), as the pi and
|
|
19
19
|
* claude adapters do. */
|
|
@@ -33,6 +33,53 @@ const defaultFetch = async (url, timeoutMs) => {
|
|
|
33
33
|
export async function fetchOpencodeCatalog(fetchFn = defaultFetch) {
|
|
34
34
|
return fetchFn(CATALOG_URL, CATALOG_FETCH_TIMEOUT_MS);
|
|
35
35
|
}
|
|
36
|
+
/** The catalog root `parseOpencodeCatalog` iterates: models.dev's live api.json puts the provider
|
|
37
|
+
* map at the top level, OpenCode's cached copy wraps that same map under a `body` key. */
|
|
38
|
+
function catalogRoot(body) {
|
|
39
|
+
const root = isJsonObject(body) ? body : {};
|
|
40
|
+
// models.dev's api.json IS the provider map; OpenCode's cached copy wraps that map under `body`.
|
|
41
|
+
const wrapped = root.body;
|
|
42
|
+
return isJsonObject(wrapped) ? wrapped : root;
|
|
43
|
+
}
|
|
44
|
+
/** The free/priced reads from a per-model cost block: whether it is present and yields two zero
|
|
45
|
+
* per-M prices. A missing cost block is unknown price data, and treating it as $0 is the unsafe
|
|
46
|
+
* direction for a budget gate. */
|
|
47
|
+
function modelPricing(m) {
|
|
48
|
+
const cost = isJsonObject(m.cost) ? m.cost : undefined;
|
|
49
|
+
const input = cost?.input ?? 0;
|
|
50
|
+
const output = cost?.output ?? 0;
|
|
51
|
+
return {
|
|
52
|
+
costed: cost !== undefined,
|
|
53
|
+
free: cost !== undefined && input === 0 && output === 0,
|
|
54
|
+
inputPerM: input,
|
|
55
|
+
outputPerM: output,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The effort levels from a per-model object: the `values` of the `reasoning_options` entries
|
|
59
|
+
* whose `type` is `"effort"`. */
|
|
60
|
+
function modelEfforts(m) {
|
|
61
|
+
if (!Array.isArray(m.reasoning_options))
|
|
62
|
+
return [];
|
|
63
|
+
return m.reasoning_options.flatMap((r) => isJsonObject(r) && r.type === "effort" && Array.isArray(r.values) ? r.values : []);
|
|
64
|
+
}
|
|
65
|
+
/** Build one catalog model from an untrusted per-model object, null when it is malformed. */
|
|
66
|
+
function catalogModelFrom(m) {
|
|
67
|
+
const pricing = modelPricing(m);
|
|
68
|
+
const model = { free: pricing.free };
|
|
69
|
+
if (pricing.costed) {
|
|
70
|
+
model.inputPerM = pricing.inputPerM;
|
|
71
|
+
model.outputPerM = pricing.outputPerM;
|
|
72
|
+
}
|
|
73
|
+
if (isJsonObject(m.limit) && m.limit.context !== undefined)
|
|
74
|
+
model.context = m.limit.context;
|
|
75
|
+
if (isJsonObject(m.modalities) && Array.isArray(m.modalities.input)) {
|
|
76
|
+
model.images = m.modalities.input.includes("image");
|
|
77
|
+
}
|
|
78
|
+
const efforts = modelEfforts(m);
|
|
79
|
+
if (efforts.length > 0)
|
|
80
|
+
model.efforts = efforts;
|
|
81
|
+
return isOpencodeCatalogModel(model) ? model : null;
|
|
82
|
+
}
|
|
36
83
|
/** Parse models.dev's `api.json` into the models the config routes through OpenCode: always the
|
|
37
84
|
* `opencode` provider, plus every provider named in `providers` (the config's
|
|
38
85
|
* `opencode/<provider>/…` routes — `opencode-go` and so on). The live `GET /api.json` returns
|
|
@@ -43,11 +90,7 @@ export async function fetchOpencodeCatalog(fetchFn = defaultFetch) {
|
|
|
43
90
|
* provider or model is skipped, never thrown on. */
|
|
44
91
|
export function parseOpencodeCatalog(body, providers) {
|
|
45
92
|
const out = {};
|
|
46
|
-
const
|
|
47
|
-
// models.dev's api.json IS the provider map; OpenCode's cached copy wraps that map under `body`.
|
|
48
|
-
const wrapped = root.body;
|
|
49
|
-
const catalog = isJsonObject(wrapped) ? wrapped : root;
|
|
50
|
-
for (const [provider, providerInfo] of Object.entries(catalog)) {
|
|
93
|
+
for (const [provider, providerInfo] of Object.entries(catalogRoot(body))) {
|
|
51
94
|
if (provider !== "opencode" && !providers.includes(provider))
|
|
52
95
|
continue;
|
|
53
96
|
if (!isJsonObject(providerInfo) || !isJsonObject(providerInfo.models))
|
|
@@ -55,28 +98,8 @@ export function parseOpencodeCatalog(body, providers) {
|
|
|
55
98
|
for (const [id, m] of Object.entries(providerInfo.models)) {
|
|
56
99
|
if (!isJsonObject(m))
|
|
57
100
|
continue;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const cost = isJsonObject(m.cost) ? m.cost : undefined;
|
|
61
|
-
const input = cost?.input ?? 0;
|
|
62
|
-
const output = cost?.output ?? 0;
|
|
63
|
-
const model = { free: cost !== undefined && input === 0 && output === 0 };
|
|
64
|
-
if (cost !== undefined) {
|
|
65
|
-
model.inputPerM = input;
|
|
66
|
-
model.outputPerM = output;
|
|
67
|
-
}
|
|
68
|
-
if (isJsonObject(m.limit) && m.limit.context !== undefined)
|
|
69
|
-
model.context = m.limit.context;
|
|
70
|
-
if (isJsonObject(m.modalities) && Array.isArray(m.modalities.input)) {
|
|
71
|
-
model.images = m.modalities.input.includes("image");
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(m.reasoning_options)) {
|
|
74
|
-
const efforts = m.reasoning_options
|
|
75
|
-
.flatMap((r) => isJsonObject(r) && r.type === "effort" && Array.isArray(r.values) ? r.values : []);
|
|
76
|
-
if (efforts.length > 0)
|
|
77
|
-
model.efforts = efforts;
|
|
78
|
-
}
|
|
79
|
-
if (isOpencodeCatalogModel(model))
|
|
101
|
+
const model = catalogModelFrom(m);
|
|
102
|
+
if (model !== null)
|
|
80
103
|
out[`${provider}/${id}`] = model;
|
|
81
104
|
}
|
|
82
105
|
}
|
|
@@ -38,29 +38,47 @@ const DENIED_PERMISSIONS = [
|
|
|
38
38
|
function isCommentStart(src, i) {
|
|
39
39
|
return src[i] === "/" && (src[i + 1] === "/" || src[i + 1] === "*");
|
|
40
40
|
}
|
|
41
|
+
/** True when `c` is a JSONC whitespace character. */
|
|
42
|
+
function isWhitespace(c) {
|
|
43
|
+
return c === " " || c === "\t" || c === "\n" || c === "\r";
|
|
44
|
+
}
|
|
45
|
+
/** Advance `j` past a `//` line comment (a caller-supplied position where `src[j] === "/"`
|
|
46
|
+
* and `src[j + 1] === "/"`), not past the terminating newline. */
|
|
47
|
+
function skipLineComment(src, j) {
|
|
48
|
+
while (j < src.length && src[j] !== "\n")
|
|
49
|
+
j += 1;
|
|
50
|
+
return j;
|
|
51
|
+
}
|
|
52
|
+
/** Advance `j` past a `/*`-style block comment (a caller-supplied position where `src[j] ===
|
|
53
|
+
* "/"` and `src[j + 1] === "*"`), ending after its closing two characters. An unterminated
|
|
54
|
+
* comment leaves `j` at `src.length + 2`, overshooting past the end — today's behaviour, kept. */
|
|
55
|
+
function skipBlockComment(src, j) {
|
|
56
|
+
j += 2;
|
|
57
|
+
while (j < src.length && !(src[j] === "*" && src[j + 1] === "/"))
|
|
58
|
+
j += 1;
|
|
59
|
+
j += 2;
|
|
60
|
+
return j;
|
|
61
|
+
}
|
|
62
|
+
/** Advance `j` past one comment, dispatching on the second character: `//` to the line-comment
|
|
63
|
+
* scan, `/*` to the block-comment scan (a caller-supplied position where `isCommentStart`
|
|
64
|
+
* holds). */
|
|
65
|
+
function skipComment(src, j) {
|
|
66
|
+
return src[j + 1] === "/" ? skipLineComment(src, j) : skipBlockComment(src, j);
|
|
67
|
+
}
|
|
41
68
|
/** Advance `j` past any whitespace and comments, for a position that is never inside a string.
|
|
42
69
|
* Returns the index of the next significant (non-comment, non-whitespace) character, or
|
|
43
70
|
* `src.length` when the input ends in comments/whitespace. */
|
|
44
71
|
function skipWhitespaceAndComments(src, j) {
|
|
45
72
|
while (j < src.length) {
|
|
46
|
-
|
|
47
|
-
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
73
|
+
if (isWhitespace(src[j])) {
|
|
48
74
|
j += 1;
|
|
49
|
-
continue;
|
|
50
75
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
j += 1;
|
|
54
|
-
continue;
|
|
76
|
+
else if (isCommentStart(src, j)) {
|
|
77
|
+
j = skipComment(src, j);
|
|
55
78
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
while (j < src.length && !(src[j] === "*" && src[j + 1] === "/"))
|
|
59
|
-
j += 1;
|
|
60
|
-
j += 2;
|
|
61
|
-
continue;
|
|
79
|
+
else {
|
|
80
|
+
break;
|
|
62
81
|
}
|
|
63
|
-
break;
|
|
64
82
|
}
|
|
65
83
|
return j;
|
|
66
84
|
}
|
|
@@ -14,22 +14,21 @@
|
|
|
14
14
|
* the model to pass `timeout` for the check (§5). `XDG_CONFIG_HOME` and its siblings are NOT
|
|
15
15
|
* set: OpenCode's shell-tool children inherit the environment, and pointing XDG at a private
|
|
16
16
|
* dir would stop `git` reading `~/.config/git/config`, losing commits' author identity (§3). */
|
|
17
|
+
import { scrubEnv } from "../env-scrub.js";
|
|
17
18
|
/** Build the opencode process environment from `base`. Never mutates `base`. */
|
|
18
19
|
export function opencodeEnv(base, opts) {
|
|
19
|
-
|
|
20
|
-
for (const [key, value] of Object.entries(base)) {
|
|
20
|
+
return scrubEnv(base, {
|
|
21
21
|
// Everything an inherited OpenCode session set is scrubbed — server, config, database,
|
|
22
22
|
// client and session id — except the operator's API key, which is operator intent and must
|
|
23
23
|
// survive. `AI_AGENT` goes too, so the shell tool sets its own (finding 16).
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return env;
|
|
24
|
+
keep: (key) => !(key === "OPENCODE" || (key.startsWith("OPENCODE_") && key !== "OPENCODE_API_KEY") || key === "AI_AGENT"),
|
|
25
|
+
then: (env) => {
|
|
26
|
+
env.OPENCODE_DISABLE_CLAUDE_CODE = "1";
|
|
27
|
+
env.OPENCODE_DISABLE_CLAUDE_CODE_SKILLS = "1";
|
|
28
|
+
env.OPENCODE_DISABLE_AUTOUPDATE = "1";
|
|
29
|
+
env.OPENCODE_DISABLE_FILEWATCHER = "1";
|
|
30
|
+
env.OPENCODE_DB = opts.dbPath;
|
|
31
|
+
env.OPENCODE_CONFIG_CONTENT = opts.configContent;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
35
34
|
}
|
|
@@ -28,49 +28,71 @@ const TOOL_NAMES = {
|
|
|
28
28
|
/** A Retry-After hint spelled out in a rate-limit error's message text ("retry after 30s" /
|
|
29
29
|
* "retry in 30 seconds"). */
|
|
30
30
|
const RETRY_AFTER = /retry (?:after|in) (\d+)\s*s(?:econds?)?\b/i;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const type = typeof err.type === "string" ? err.type : "";
|
|
39
|
-
const status = err.status;
|
|
40
|
-
// The free tier refuses intermittently (a provider failure, not a config error): three
|
|
41
|
-
// refusals in a row move the tier to its fallback through the existing model-failure
|
|
42
|
-
// episode. Typed `provider.auth` WITH status 403 — the free tier's own refusal — or the
|
|
43
|
-
// text naming the free tier. A `provider.auth` with any other status (a 401 bad
|
|
44
|
-
// credential) is not a refusal and falls through to the config-error row below.
|
|
31
|
+
;
|
|
32
|
+
/** Is this the free tier's intermittent refusal (a provider failure, not a config error)?
|
|
33
|
+
* Three refusals in a row move the tier to its fallback through the existing model-failure
|
|
34
|
+
* episode. Typed `provider.auth` WITH status 403 — the free tier's own refusal — or the
|
|
35
|
+
* text naming the free tier. A `provider.auth` with any other status (a 401 bad
|
|
36
|
+
* credential) is not a refusal and falls through to the config-error row. */
|
|
37
|
+
function freeTierClassification(type, status, message) {
|
|
45
38
|
if ((type === "provider.auth" && status === 403) || /free\s?tier|FreeTierError/i.test(message))
|
|
46
39
|
return { kind: "transientBackend", backendKind: "server", freeTier: true };
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Is this a rate limit (429 or rate-limit text) — the world saying "later", inherently
|
|
43
|
+
* retryable? Carry the echoed Retry-After hint when the message spells it. */
|
|
44
|
+
function rateLimitClassification(status, message) {
|
|
49
45
|
if (status === 429 || /\b429\b|rate limit|too many requests/i.test(message)) {
|
|
50
46
|
const retry = RETRY_AFTER.exec(message);
|
|
51
47
|
return { kind: "transientRateLimit", ...(retry ? { retryAfterSeconds: Number(retry[1]) } : {}) };
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Is this a permanent config error — an unknown model or variant, or the malformed `ses_`
|
|
52
|
+
* id (a misconfiguration, not a transient outage): typed `provider.no-route` / `unknown`,
|
|
53
|
+
* or the model-not-found / 401 text? */
|
|
54
|
+
function configErrorClassification(type, status, message) {
|
|
56
55
|
if (type === "provider.no-route" ||
|
|
57
56
|
type === "unknown" ||
|
|
58
57
|
status === 401 ||
|
|
59
58
|
/unknown model|unknown provider|model not found|provider not found|unauthorized|status\s*401|\b401\b/i.test(message))
|
|
60
59
|
return { kind: "configError" };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Did context overflow in a way compaction could not fix? Matched STRICTLY (the same shape
|
|
63
|
+
* pi's own CONTEXT_ERROR matches), so a message that merely mentions "context" or "too long"
|
|
64
|
+
* in passing — a plugin error, say — is never misread as an overflow that would then be
|
|
65
|
+
* trusted by PiStreamParser over its own tighter regex. */
|
|
66
|
+
function contextOverflowClassification(message) {
|
|
65
67
|
if (/context (size|length|window)?\s*(has been |was )?exceeded|exceeds? (the )?context|too (long|large) for .*context/i.test(message))
|
|
66
68
|
return { kind: "contextExceeded" };
|
|
67
|
-
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Is this a transient server, connection or timeout failure? The server statuses and text
|
|
72
|
+
* first, then the shared connection/timeout prose vocabulary (see `transientProseKind`). */
|
|
73
|
+
function transientClassification(message) {
|
|
68
74
|
if (/\b(500|501|502|503|504|529)\b|internal server error|bad gateway|service unavailable|gateway timeout|overloaded/i.test(message))
|
|
69
75
|
return { kind: "transientBackend", backendKind: "server" };
|
|
70
76
|
const prose = transientProseKind(message);
|
|
71
77
|
if (prose)
|
|
72
78
|
return prose;
|
|
73
|
-
return
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/** How an `error` line classifies into the canonical `tumwaterKind` (and, for a transient
|
|
82
|
+
* backend, its kind), trusted by `PiStreamParser` before its text regexes. The error's
|
|
83
|
+
* `type` — the provider signal the plan's table maps (provider.auth, provider.no-route,
|
|
84
|
+
* unknown) — is consulted FIRST, the message text second (plans/opencode-backend.md §8 and
|
|
85
|
+
* its classification table). */
|
|
86
|
+
function classifyError(err) {
|
|
87
|
+
const message = typeof err.message === "string" ? err.message : "";
|
|
88
|
+
const type = typeof err.type === "string" ? err.type : "";
|
|
89
|
+
const status = err.status;
|
|
90
|
+
return (freeTierClassification(type, status, message) ??
|
|
91
|
+
rateLimitClassification(status, message) ??
|
|
92
|
+
configErrorClassification(type, status, message) ??
|
|
93
|
+
contextOverflowClassification(message) ??
|
|
94
|
+
transientClassification(message) ??
|
|
95
|
+
{});
|
|
74
96
|
}
|
|
75
97
|
/** The step's token counts and cost → pi's canonical usage block: the four counts, their sum
|
|
76
98
|
* as `totalTokens`, and `cost.total` = the step's USD cost (plans/opencode-backend.md §6).
|
|
@@ -39,7 +39,7 @@ export function recordSession(dir, record) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
/** The one run's ledger record, minus the id the two recording sites mint differently — the
|
|
42
|
-
* pre-spawn id in
|
|
42
|
+
* pre-spawn id in agent-spawn.ts, or the stream-minted session id in agent-stream.ts. One place
|
|
43
43
|
* for the record's non-id shape (agent, kind, name, cwd, the optional model and the stamp), so
|
|
44
44
|
* the two appends cannot drift on the model spread or the required fields. `agent` is the
|
|
45
45
|
* owning backend's id; the run options supply the rest. */
|
|
@@ -31,9 +31,9 @@ export function subscriptionWindowRejected(run) {
|
|
|
31
31
|
/** The subscription account's used-up usage window, or null: a window reading `rejected` with
|
|
32
32
|
* no overage, or (with `includeOverage`, the default) a window whose current reading shows
|
|
33
33
|
* `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
|
|
34
|
-
* last. With a `reserve` (
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* last. With a `reserve` (the subscription-window reserve), a window whose utilization has
|
|
35
|
+
* reached its type's fraction counts too and comes back with `reserve: true`; a rejected or
|
|
36
|
+
* overage window outranks a reserve one, so the flag is set only when no window is used up
|
|
37
37
|
* for real. The budget gate's window move (pollBudgetGate) reads the default; the team hold
|
|
38
38
|
* (windowObservation) passes `includeOverage: false`, because overage never holds a
|
|
39
39
|
* seam. Only subscription billing produces one; a missing account reads null. */
|
|
@@ -10,6 +10,12 @@ import { groupBlockedSeries, partsLabel } from "./backlog-index-data.js";
|
|
|
10
10
|
import { rejectUnknownArgs } from "../cli/cli-flag-specs.js";
|
|
11
11
|
import { refuseTeamWorkspace } from "../cli/cli-repo-gate.js";
|
|
12
12
|
import { sectionNoun, showEntry } from "./backlog-write.js";
|
|
13
|
+
/** `bug --list --done -n` / `plan --list --done -n` ceiling: bounds how many finished
|
|
14
|
+
* entries one list shows, so a typo'd huge count fails fast with the shared `must be between
|
|
15
|
+
* 1 and <max>` wording (the logs/history/report pattern) instead of silently meaning
|
|
16
|
+
* "everything". The finished lists are small (PLANS.md/BUGS.md), so the cap never meaningfully
|
|
17
|
+
* binds — it exists for the uniform count-flag contract and the clear over-range rebuke. */
|
|
18
|
+
const BACKLOG_MAX_LISTED = 100_000;
|
|
13
19
|
/** Ascending positions as runs: consecutive ones join with an en dash (`13–17`), runs separate
|
|
14
20
|
* with commas (`13–15, 18`). */
|
|
15
21
|
function positionRuns(positions) {
|
|
@@ -59,7 +65,7 @@ function parseListArgs(args, command) {
|
|
|
59
65
|
}
|
|
60
66
|
if (arg === "-n") {
|
|
61
67
|
rejectRepeatFlag(count !== null, "-n");
|
|
62
|
-
count = parseCountFlag("-n", args[++i]);
|
|
68
|
+
count = parseCountFlag("-n", args[++i], BACKLOG_MAX_LISTED);
|
|
63
69
|
continue;
|
|
64
70
|
}
|
|
65
71
|
if (arg === "--list") {
|
|
@@ -114,10 +120,10 @@ function announceFinishedList(root, fileName, bugSection, grep, count, json) {
|
|
|
114
120
|
const header = pattern === null ? `${what} in ${fileName}, newest first:` : `${what} in ${fileName} matching "${pattern}", newest first:`;
|
|
115
121
|
say(`${header}\n${lines.join("\n")}`);
|
|
116
122
|
}
|
|
117
|
-
/** The active
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
function
|
|
123
|
+
/** The active list's read-and-filter phase: reads the section's entries and each position's
|
|
124
|
+
* hold and live claimer, numbers them unfiltered, keeps the `--grep` matches under those
|
|
125
|
+
* positions, drops to the `--ready` survivors, counts what `--ready` hid, and names the noun. */
|
|
126
|
+
function activeListView(root, bugSection, sectionTitle, grep, ready) {
|
|
121
127
|
const entries = bugSection ? openBugEntries(root) : plannedPlanEntries(root);
|
|
122
128
|
const holds = backlogHolds(root);
|
|
123
129
|
const entryHolds = bugSection ? holds.bugs : holds.plans;
|
|
@@ -128,47 +134,62 @@ function announceActiveList(root, fileName, command, bugSection, sectionTitle, g
|
|
|
128
134
|
const numbered = entries.map((entry, i) => ({ entry, position: i + 1 }));
|
|
129
135
|
const matches = grep === null ? numbered : numbered.filter((m) => entryMatches(m.entry, grep));
|
|
130
136
|
const kept = ready ? matches.filter((m) => (entryHolds[m.position - 1] ?? null) === null) : matches;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
137
|
+
return {
|
|
138
|
+
entries,
|
|
139
|
+
entryHolds,
|
|
140
|
+
entryRunners,
|
|
141
|
+
matches,
|
|
142
|
+
kept,
|
|
143
|
+
hidden: matches.length - kept.length,
|
|
144
|
+
noun: sectionNoun(sectionTitle),
|
|
145
|
+
grep,
|
|
146
|
+
ready,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The `--json` body: each kept entry as its position, title, verbatim body, hold,
|
|
150
|
+
* in-progress claim and — only when the heading names a filing stamp — its `since`/`ageDays`. */
|
|
151
|
+
function activeListJson(view) {
|
|
152
|
+
const { kept, entryHolds, entryRunners } = view;
|
|
153
|
+
return {
|
|
154
|
+
entries: kept.map(({ entry, position }) => {
|
|
155
|
+
const since = filingDate(entry.title);
|
|
156
|
+
return {
|
|
157
|
+
position,
|
|
158
|
+
title: entry.title,
|
|
159
|
+
body: entry.body,
|
|
160
|
+
hold: entryHolds[position - 1] ?? null,
|
|
161
|
+
inProgress: entryRunners[position - 1] ?? null,
|
|
162
|
+
// The filing stamp the entry's own heading carries, and how many days it has waited
|
|
163
|
+
// since then (both present only when the heading names a stamp — an undated entry
|
|
164
|
+
// keeps the pre-existing shape with neither key).
|
|
165
|
+
...(since === null ? {} : { since, ageDays: ageInDays(since) }),
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** The prose `no …` line for an empty list, a `--grep` with no match, or a `--ready` list
|
|
171
|
+
* whose every match is held; null when `view.kept` has rows to print. */
|
|
172
|
+
function activeListEmptyLine(view, command) {
|
|
173
|
+
const { entries, matches, kept, hidden, noun, grep, ready } = view;
|
|
174
|
+
if (grep === null && entries.length === 0)
|
|
175
|
+
return `no ${noun}s`;
|
|
176
|
+
if (matches.length === 0)
|
|
177
|
+
return `no ${noun}s match "${grep}"`;
|
|
178
|
+
if (ready && kept.length === 0)
|
|
179
|
+
return `no ${noun}s ready (${hidden} held; run \`tumwater ${command} --list\` to see them)`;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/** The prose body of a non-empty active list: the bug list's numbered `line` rows, or the
|
|
183
|
+
* plan list's `groupBlockedSeries` rows, each with the hidden-held notice when `hidden > 0`. */
|
|
184
|
+
function activeListProse(view, fileName, command, bugSection) {
|
|
185
|
+
const { kept, entryHolds, entryRunners, hidden, noun } = view;
|
|
164
186
|
const hiddenNote = `(${hidden} held entries hidden; run \`tumwater ${command} --list\` to see them)`;
|
|
165
187
|
const line = (entry, i) => `${entry.title}${holdMark(entryHolds[i] ?? null)}${inProgressMark(entryRunners[i] ?? null)}${entryAgeSuffix(entry.title)}`;
|
|
166
188
|
if (bugSection) {
|
|
167
189
|
const bugLines = kept.map(({ entry, position }) => `${position}. ${line(entry, position - 1)}`);
|
|
168
190
|
if (hidden > 0)
|
|
169
191
|
bugLines.push(hiddenNote);
|
|
170
|
-
|
|
171
|
-
return;
|
|
192
|
+
return `${noun}s in ${fileName}:\n${bugLines.join("\n")}`;
|
|
172
193
|
}
|
|
173
194
|
// Plans: a series' blocked parts show as one line naming their positions, as the prompt's
|
|
174
195
|
// backlog index does; numbering is unchanged so --show/--edit/--cancel <n> reach every part.
|
|
@@ -189,7 +210,26 @@ function announceActiveList(root, fileName, command, bugSection, sectionTitle, g
|
|
|
189
210
|
lines.push("(each blocked series is one line; --show <n> prints a part, --json lists every entry)");
|
|
190
211
|
if (hidden > 0)
|
|
191
212
|
lines.push(hiddenNote);
|
|
192
|
-
|
|
213
|
+
return `${noun}s in ${fileName}:\n${lines.join("\n")}`;
|
|
214
|
+
}
|
|
215
|
+
/** The active half of the list shell: the open entries with their holds, in-progress claim
|
|
216
|
+
* marks and age suffixes, optionally reduced to the ready ones, as `--json` or prose, grouping
|
|
217
|
+
* a plan's blocked parts into one line (groupBlockedSeries). Orchestrates the four named
|
|
218
|
+
* phases below — the read-and-filter view (activeListView), the `--json` body (activeListJson),
|
|
219
|
+
* the empty-list prose line (activeListEmptyLine) and the bug/plan prose body
|
|
220
|
+
* (activeListProse). */
|
|
221
|
+
function announceActiveList(root, fileName, command, bugSection, sectionTitle, grep, ready, json) {
|
|
222
|
+
const view = activeListView(root, bugSection, sectionTitle, grep, ready);
|
|
223
|
+
if (json) {
|
|
224
|
+
sayJson(activeListJson(view));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const empty = activeListEmptyLine(view, command);
|
|
228
|
+
if (empty !== null) {
|
|
229
|
+
say(empty);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
say(activeListProse(view, fileName, command, bugSection));
|
|
193
233
|
}
|
|
194
234
|
/** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
|
|
195
235
|
* operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
|