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
|
@@ -263,12 +263,13 @@ function sourceHaystackSignature(root) {
|
|
|
263
263
|
sigs.sort();
|
|
264
264
|
return sigs.join("|");
|
|
265
265
|
}
|
|
266
|
-
/** Every file under the
|
|
267
|
-
*
|
|
266
|
+
/** Every file under the tree the haystack reads — the whole root, so a project whose code lives
|
|
267
|
+
* outside src//test//scripts/ (tools/, tests/, lib/, …) backs its symbols like a src/ one — with
|
|
268
|
+
* build output, vendored code, VCS and harness state skipped at any depth: node_modules, dist,
|
|
269
|
+
* .git and .tumwater. */
|
|
268
270
|
function walkSourceFiles(root, visit) {
|
|
269
|
-
const skipDirs = new Set(["node_modules", "dist"]);
|
|
270
|
-
|
|
271
|
-
walkFiles(join(root, dir), visit, skipDirs);
|
|
271
|
+
const skipDirs = new Set(["node_modules", "dist", ".git", ".tumwater"]);
|
|
272
|
+
walkFiles(root, visit, skipDirs);
|
|
272
273
|
}
|
|
273
274
|
/** A slash-free name with a file extension: `status-roles.ts`, not `src/a.ts` or `signalTree`. */
|
|
274
275
|
const FILE_NAME_RE = /^[^/]+\.[A-Za-z][\w-]*$/;
|
|
@@ -297,23 +298,15 @@ export function unbackedSymbols(root, symbols, haystack) {
|
|
|
297
298
|
export function missingSymbolNames(missing) {
|
|
298
299
|
return missing.length <= 3 ? missing.join(", ") : `${missing.slice(0, 3).join(", ")}…`;
|
|
299
300
|
}
|
|
300
|
-
/** The
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return undefined;
|
|
310
|
-
// A deleted, unreadable, or non-regular (symlinked) file reads as empty: there is no Fixed
|
|
311
|
-
// transition to verify, so this check passes and lets the gate's sibling structural check
|
|
312
|
-
// (backlog-structure.ts) report the dropped section. Reading a symlink raw would pull an
|
|
313
|
-
// outside host file's `## Fixed` heading into this gate's verdict (the same lstat policy the
|
|
314
|
-
// backlog-conflict resolver and backlogStructureReason pin under INV-G3), so a head is read
|
|
315
|
-
// only when lstat shows a regular file; ENOENT and symlinks both read as empty.
|
|
316
|
-
const heads = ["BUGS.md", BUGS_ARCHIVE].map((f) => {
|
|
301
|
+
/** The `## Fixed` heads of BUGS.md and its archive as read under the lstat policy: a deleted,
|
|
302
|
+
* unreadable, or non-regular (symlinked) file reads as empty — there is no Fixed transition to
|
|
303
|
+
* verify, so the gate's sibling structural check (backlog-structure.ts) reports the dropped
|
|
304
|
+
* section instead. Reading a symlink raw would pull an outside host file's `## Fixed` heading
|
|
305
|
+
* into the gate's verdict (the same lstat policy the backlog-conflict resolver and
|
|
306
|
+
* backlogStructureReason pin under INV-G3), so a head is read only when lstat shows a regular
|
|
307
|
+
* file; ENOENT and symlinks both read as empty. */
|
|
308
|
+
function readFixedHeads(wt) {
|
|
309
|
+
return ["BUGS.md", BUGS_ARCHIVE].map((f) => {
|
|
317
310
|
const full = join(wt, f);
|
|
318
311
|
try {
|
|
319
312
|
if (!lstatSync(full).isFile())
|
|
@@ -324,40 +317,40 @@ export async function falseFixReason(wt, mainBranch, files) {
|
|
|
324
317
|
}
|
|
325
318
|
return readTextOrNull(full) ?? "";
|
|
326
319
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
320
|
+
}
|
|
321
|
+
/** The base Fixed section bodies the diff's own merge-base holds, keyed by normalized heading.
|
|
322
|
+
* The base is the diff's own merge-base, not main's tip: the gate diffs `mainBranch...HEAD`
|
|
323
|
+
* against the merge-base, and the entry's Open/Fixed state lives in the tree the change builds
|
|
324
|
+
* on — pre-batch main can still carry the record as (falsely) Fixed and make an Open→Fixed
|
|
325
|
+
* restoration in a stacked batch compare as already done (BUGS.md 2026-09-23). Every base body
|
|
326
|
+
* under a heading counts: two distinct entries can share one normalized heading, and keeping
|
|
327
|
+
* only the last made the other read as rewritten (BUGS.md 2026-10-23). */
|
|
328
|
+
async function baseFixedBodies(wt, mainBranch) {
|
|
333
329
|
const baseRev = await changeBaseRev(wt, mainBranch);
|
|
334
330
|
const bases = [await fileContentAt(wt, baseRev, "BUGS.md"), await fileContentAt(wt, baseRev, BUGS_ARCHIVE)];
|
|
335
|
-
|
|
336
|
-
// rewrites an existing Fixed record's narrative must face the symbol check too (BUGS.md
|
|
337
|
-
// 2026-09-23 — the in-place narrative rewrite is how a second phantom merge evaded it).
|
|
338
|
-
// Every base body under a heading counts: two distinct entries can share one normalized
|
|
339
|
-
// heading, and keeping only the last made the other read as rewritten (BUGS.md 2026-10-23).
|
|
340
|
-
const baseBodies = new Map();
|
|
331
|
+
const bodies = new Map();
|
|
341
332
|
for (const e of bases.flatMap((base) => fixedEntries(base))) {
|
|
342
333
|
const key = normalizeFixedHeading(e.title);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
334
|
+
const known = bodies.get(key) ?? new Set();
|
|
335
|
+
known.add(e.body);
|
|
336
|
+
bodies.set(key, known);
|
|
346
337
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
338
|
+
return { bases, bodies };
|
|
339
|
+
}
|
|
340
|
+
/** A changed full Fixed entry's rejection, or undefined when every changed entry backs its
|
|
341
|
+
* symbols. An already-Fixed entry is skipped only when its body is unchanged: an md-only edit
|
|
342
|
+
* that rewrites an existing Fixed record's narrative must face the symbol check too (BUGS.md
|
|
343
|
+
* 2026-09-23 — the in-place narrative rewrite is how a second phantom merge evaded it). The
|
|
344
|
+
* haystack is built lazily — only once a Fixed record's body actually changed — because it
|
|
345
|
+
* walks and reads the entire source tree (~5 MB here), and the common BUGS.md edit moves
|
|
346
|
+
* nothing to Fixed: an Open bug added, a narrative touched elsewhere, so the gate and the
|
|
347
|
+
* in-lock recheck each paid that walk for a check that never reached a symbol. */
|
|
348
|
+
function movedEntryReason(wt, entries, bodies, getHaystack) {
|
|
355
349
|
for (const { title: heading, body } of entries) {
|
|
356
|
-
const knownBodies =
|
|
350
|
+
const knownBodies = bodies.get(normalizeFixedHeading(heading));
|
|
357
351
|
if (knownBodies?.has(body))
|
|
358
352
|
continue;
|
|
359
|
-
|
|
360
|
-
const missing = unbackedSymbols(wt, fixSymbols(body), haystack);
|
|
353
|
+
const missing = unbackedSymbols(wt, fixSymbols(body), getHaystack());
|
|
361
354
|
if (missing.length === 0)
|
|
362
355
|
continue;
|
|
363
356
|
const names = missingSymbolNames(missing);
|
|
@@ -372,10 +365,15 @@ export async function falseFixReason(wt, mainBranch, files) {
|
|
|
372
365
|
`paragraph now names code that exists nowhere on this tree: ${names} — include that ` +
|
|
373
366
|
`code in the same commit, or leave the entry's text as it was`;
|
|
374
367
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
/** An added or edited one-line Fixed record's rejection, or undefined when each record either
|
|
371
|
+
* reproduces a base line or carries only names the base already holds. One-line records are
|
|
372
|
+
* compressions of entries the base already holds as Fixed, and history keeps a renamed symbol's
|
|
373
|
+
* old name, so a record the diff adds or edits may carry forward any name the base's Fixed
|
|
374
|
+
* section already records. A name it introduces must exist on the tree: a bug moved straight
|
|
375
|
+
* from Open into a one-line record faces the same check as a full entry. */
|
|
376
|
+
function addedRecordReason(wt, records, bases, getHaystack) {
|
|
379
377
|
const baseLines = bases.flatMap((base) => sectionLines(base, "Fixed")).map((line) => line.trim());
|
|
380
378
|
const seen = new Set(baseLines);
|
|
381
379
|
const baseText = baseLines.join("\n");
|
|
@@ -385,8 +383,7 @@ export async function falseFixReason(wt, mainBranch, files) {
|
|
|
385
383
|
const introduced = fixSymbols(record).filter((symbol) => !baseText.includes(symbol));
|
|
386
384
|
if (introduced.length === 0)
|
|
387
385
|
continue;
|
|
388
|
-
|
|
389
|
-
const missing = unbackedSymbols(wt, introduced, haystack);
|
|
386
|
+
const missing = unbackedSymbols(wt, introduced, getHaystack());
|
|
390
387
|
if (missing.length === 0)
|
|
391
388
|
continue;
|
|
392
389
|
return (`md-only BUGS.md edit adds the one-line Fixed record "${truncate(record, RECORD_QUOTE_MAX)}", ` +
|
|
@@ -396,3 +393,28 @@ export async function falseFixReason(wt, mainBranch, files) {
|
|
|
396
393
|
}
|
|
397
394
|
return undefined;
|
|
398
395
|
}
|
|
396
|
+
/** The first false-fix claim in an md-only diff that touches BUGS.md, or undefined when the
|
|
397
|
+
* diff backs its Fixed transitions (or makes none). `files` is the ahead-of-main path list
|
|
398
|
+
* of the diff merging on `mainBranch`; the worktree `wt` holds the tree being merged. */
|
|
399
|
+
export async function falseFixReason(wt, mainBranch, files) {
|
|
400
|
+
// Fixed entries live in BUGS.md's own section and in its archive, BUGS-FIXED.md
|
|
401
|
+
// (plans/backlog-archive.md): tumwater moves a fixed bug there right before the commit, so a
|
|
402
|
+
// fix claim reaches the gate as an archive edit, and an archived entry whose body is intact
|
|
403
|
+
// is no claim at all. Both sides of the comparison read both files.
|
|
404
|
+
if (!files.includes("BUGS.md") && !files.includes(BUGS_ARCHIVE))
|
|
405
|
+
return undefined;
|
|
406
|
+
const heads = readFixedHeads(wt);
|
|
407
|
+
if (!heads.some((head) => head.includes("## Fixed")))
|
|
408
|
+
return undefined;
|
|
409
|
+
const { bases, bodies } = await baseFixedBodies(wt, mainBranch);
|
|
410
|
+
const sections = heads.map((head) => fixedSection(head));
|
|
411
|
+
const entries = sections.flatMap((section) => section.entries);
|
|
412
|
+
const records = sections.flatMap((section) => section.records);
|
|
413
|
+
// Built lazily, shared by both checks, only once a Fixed record's body actually changed: the
|
|
414
|
+
// haystack walks and reads the entire source tree (~5 MB here), and the common BUGS.md edit
|
|
415
|
+
// moves nothing to Fixed — an Open bug added, a narrative touched elsewhere — so the gate and
|
|
416
|
+
// the in-lock recheck each paid that walk for a check that never reached a symbol.
|
|
417
|
+
let haystack;
|
|
418
|
+
const getHaystack = () => (haystack ??= sourceHaystack(wt));
|
|
419
|
+
return movedEntryReason(wt, entries, bodies, getHaystack) ?? addedRecordReason(wt, records, bases, getHaystack);
|
|
420
|
+
}
|
|
@@ -106,7 +106,7 @@ async function stripBlockedPlanRefusalNotes(ctx, wt) {
|
|
|
106
106
|
// and a plain writeFileSync that a kill interrupted would commit a truncated file,
|
|
107
107
|
// permanently dropping the plan entries past the cut point. writeTextAtomic is the
|
|
108
108
|
// harness's one write-to-temp-then-rename entry point — the same one backlog-write.ts
|
|
109
|
-
// and question-answer.ts use for the markdown backlog files.
|
|
109
|
+
// and backlog-question-answer.ts use for the markdown backlog files.
|
|
110
110
|
try {
|
|
111
111
|
writeTextAtomic(plansFile, md);
|
|
112
112
|
for (const title of dropped) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tumwater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Opinionated autonomous development harness built on pi. You write the initial prompt; role-driven loops build the project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"build": "rm -rf dist && tsc && node scripts/stamp-build.mjs",
|
|
38
38
|
"prepack": "npm run build",
|
|
39
39
|
"lint": "eslint . --cache",
|
|
40
|
-
"test": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js",
|
|
41
|
-
"test:gate": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js --affected",
|
|
42
|
-
"test:e2e": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js e2e",
|
|
43
|
-
"test:coverage": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node dist/test/test-runner.js --coverage",
|
|
40
|
+
"test": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js",
|
|
41
|
+
"test:gate": "node scripts/live-checkout-guard.mjs && eslint . --cache && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js --affected",
|
|
42
|
+
"test:e2e": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js e2e",
|
|
43
|
+
"test:coverage": "node scripts/live-checkout-guard.mjs && tsc --incremental && node scripts/stamp-build.mjs && node scripts/check-permit.mjs node dist/test/test-runner.js --coverage",
|
|
44
44
|
"smoke:claude": "node scripts/smoke-claude.mjs",
|
|
45
45
|
"smoke:opencode": "node scripts/smoke-opencode.mjs",
|
|
46
46
|
"smoke:codex": "node scripts/smoke-codex.mjs",
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/** The AsyncLocalStorage store that lets a run's lease follow its runs across providers
|
|
2
|
-
* (plans/provider-concurrency.md §5): a lease rides here for the length of a run, a verify task,
|
|
3
|
-
* or a resolver run, and `RunnerPi.runWithTransientRetry` re-keys it to the provider each run
|
|
4
|
-
* actually uses. The one choke point every run a role makes (authoring, gate, merge and resolver)
|
|
5
|
-
* passes through, so a budget handback or a model-failure switch that moves a run in flight to
|
|
6
|
-
* another provider moves the lease with it instead of leaving it parked on the key it was admitted
|
|
7
|
-
* on. The check-permit.ts `holdingPermit` store is the pattern: nothing here is a separate permit,
|
|
8
|
-
* only the single lease the run path already holds.
|
|
9
|
-
*
|
|
10
|
-
* The store carries the `piDefault` (the caller's cached readPiDefaultModel result) beside the
|
|
11
|
-
* lease so `ensureProviderLease` can resolve a bare no-model pi seam's key exactly as the
|
|
12
|
-
* admission/review seam did — a run whose key were computed without it would re-key a default
|
|
13
|
-
* team's run off pi's own provider. It also carries the semaphore and, optionally, wait
|
|
14
|
-
* hooks, so a re-key that has to park on a full target key reports its wait the way a parked
|
|
15
|
-
* resolver does (merge-verify.ts's providerWaitStage). */
|
|
16
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
-
import { seamConcurrencyKey } from "../config/config-views.js";
|
|
18
|
-
const providerLeaseStore = new AsyncLocalStorage();
|
|
19
|
-
/** Run `run` with `lease` in the provider-lease store, so any run it makes re-keys that lease to
|
|
20
|
-
* the provider it actually uses (ensureProviderLease). `opts.piDefault` is the caller's cached pi
|
|
21
|
-
* default model, so a bare no-model seam resolves to the same key the admission or review seam
|
|
22
|
-
* used; `opts.hooks` (the verify: providerWaitStage) hears a re-key wait, if there is one. The same
|
|
23
|
-
* lease object is released by the caller after the store is unwrapped — a re-key mutates it in
|
|
24
|
-
* place, so release() frees whatever key the run path ended on. */
|
|
25
|
-
export function withProviderLease(lease, semaphore, run, opts = {}) {
|
|
26
|
-
return providerLeaseStore.run({ lease, semaphore, piDefault: opts.piDefault, hooks: opts.hooks }, run);
|
|
27
|
-
}
|
|
28
|
-
/** Re-key the calling lease to the provider `cfg` runs on, if it is not there yet. No-op when
|
|
29
|
-
* the current async context carries no lease (a direct RunnerPi unit test, `tumwater history`) or
|
|
30
|
-
* when the run is already on the lease's key. When it does re-key and `hooks` are present and
|
|
31
|
-
* the target key is at its cap, the hooks hear `waiting` before the lease parks and `granted`
|
|
32
|
-
* once the key frees — a re-key never waits on the team total (it already holds a team
|
|
33
|
-
* permit), only on the target key's headroom. */
|
|
34
|
-
export async function ensureProviderLease(cfg) {
|
|
35
|
-
const ctx = providerLeaseStore.getStore();
|
|
36
|
-
if (!ctx)
|
|
37
|
-
return;
|
|
38
|
-
const key = seamConcurrencyKey(cfg, ctx.piDefault);
|
|
39
|
-
if (ctx.lease.key === key)
|
|
40
|
-
return;
|
|
41
|
-
// The leased currently holds `old` (or nothing); the target key's in-use does not include it,
|
|
42
|
-
// so the key being at its cap is exactly whether rekey() will park.
|
|
43
|
-
const parks = ctx.hooks !== undefined && keyAtCap(ctx, key);
|
|
44
|
-
if (parks)
|
|
45
|
-
ctx.hooks.waiting();
|
|
46
|
-
await ctx.lease.rekey(key);
|
|
47
|
-
if (parks)
|
|
48
|
-
ctx.hooks.granted();
|
|
49
|
-
}
|
|
50
|
-
/** Whether `key` is at its configured cap right now (an absent or uncapped key is not) — the
|
|
51
|
-
* same test semaphore.drive uses to withhold a re-key grant, decided from the live snapshot so
|
|
52
|
-
* a re-key wait can be announced before the lease parks (providerWaitStage). */
|
|
53
|
-
function keyAtCap(ctx, key) {
|
|
54
|
-
if (key === undefined)
|
|
55
|
-
return false;
|
|
56
|
-
const entry = ctx.semaphore.snapshot().byKey[key];
|
|
57
|
-
return entry?.limit !== undefined && entry.inUse >= entry.limit;
|
|
58
|
-
}
|
|
59
|
-
/** The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds a
|
|
60
|
-
* provider key while it waits for a worktree pool slot. Called by leaseSlot just before it parks on
|
|
61
|
-
* a busy pool; it re-keys the calling lease to no key (which never waits), so a run or verify
|
|
62
|
-
* waiting for a slot does not hold — and cannot deadlock — a provider slot whose in-use a re-key
|
|
63
|
-
* waiter is waiting to drain. No-op without a lease in context. */
|
|
64
|
-
export async function dropProviderLeaseKey() {
|
|
65
|
-
const ctx = providerLeaseStore.getStore();
|
|
66
|
-
if (!ctx)
|
|
67
|
-
return;
|
|
68
|
-
await ctx.lease.rekey(undefined);
|
|
69
|
-
}
|
|
70
|
-
/** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit and
|
|
71
|
-
* key slot handed back (SemaphoreLease.pause), so a parked run can use them meanwhile, then take
|
|
72
|
-
* them back (resume) before returning. A failed review's in-place retry pause is the caller: with
|
|
73
|
-
* one team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
|
|
74
|
-
* other role. Once `signal` has fired, nothing is taken back and the caller's release() settles a
|
|
75
|
-
* still-paused lease. No-op without a lease in context. */
|
|
76
|
-
export async function idleWithoutProviderLease(wait, signal) {
|
|
77
|
-
const ctx = providerLeaseStore.getStore();
|
|
78
|
-
if (!ctx)
|
|
79
|
-
return wait();
|
|
80
|
-
ctx.lease.pause();
|
|
81
|
-
try {
|
|
82
|
-
await wait();
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
if (!signal?.aborted)
|
|
86
|
-
await untilAborted(ctx.lease.resume(), signal);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
|
|
90
|
-
* aborted verification waiting for a permit it no longer needs. */
|
|
91
|
-
async function untilAborted(p, signal) {
|
|
92
|
-
if (!signal)
|
|
93
|
-
return p;
|
|
94
|
-
let onAbort = () => { };
|
|
95
|
-
const aborted = new Promise((resolve) => {
|
|
96
|
-
onAbort = resolve;
|
|
97
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
98
|
-
});
|
|
99
|
-
try {
|
|
100
|
-
await Promise.race([p, aborted]);
|
|
101
|
-
}
|
|
102
|
-
finally {
|
|
103
|
-
signal.removeEventListener("abort", onAbort);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** Consecutive provider-class failures that trip a fallback episode. */
|
|
2
|
-
export const MODEL_FALLBACK_FAILURES = 3;
|
|
3
|
-
/** The first probe cooldown after a trip; a failed probe doubles it. */
|
|
4
|
-
export const MODEL_FALLBACK_COOLDOWN_MS = 5 * 60_000;
|
|
5
|
-
/** The ceiling a repeatedly-failing probe's cooldown doubles toward. */
|
|
6
|
-
export const MODEL_FALLBACK_MAX_COOLDOWN_MS = 30 * 60_000;
|
|
7
|
-
/** True only while the role is in a fallback episode whose probe is still in the future — the
|
|
8
|
-
* runs that use the fallback pair. */
|
|
9
|
-
export function modelFallbackActive(state, now) {
|
|
10
|
-
return state !== undefined && state.since > 0 && now < state.probeAt;
|
|
11
|
-
}
|
|
12
|
-
/** True only while the role is in a fallback episode whose cooldown has elapsed: the next
|
|
13
|
-
* run uses the primary as the probe. */
|
|
14
|
-
export function modelFallbackProbe(state, now) {
|
|
15
|
-
return state !== undefined && state.since > 0 && now >= state.probeAt;
|
|
16
|
-
}
|
|
17
|
-
/** Classify one authoring agent run's verdict about the primary (src/run/run-usage.ts records the
|
|
18
|
-
* same flags for the team-wide hold). An agent run that was aborted, quiet-killed, or timed out
|
|
19
|
-
* — or that read an image its model cannot see (the runner killed it, plans/image-input.md) — is
|
|
20
|
-
* inconclusive even when it also reports provider evidence: the harness killed it, so it is
|
|
21
|
-
* no verdict about the primary's health. */
|
|
22
|
-
export function modelFallbackVerdict(pi) {
|
|
23
|
-
if (pi.aborted || pi.quietKilled || pi.timedOut || pi.imageBlind)
|
|
24
|
-
return "inconclusive";
|
|
25
|
-
if (!pi.ok && (pi.transientRateLimit || pi.transientBackend))
|
|
26
|
-
return "provider-failure";
|
|
27
|
-
return "answered";
|
|
28
|
-
}
|
|
29
|
-
/** Fold one authoring agent run's verdict into the role's fallback state, returning the next state
|
|
30
|
-
* or undefined for "running the primary" (no episode).
|
|
31
|
-
*
|
|
32
|
-
* - Not in fallback, provider failure: increment the consecutive count; the
|
|
33
|
-
* MODEL_FALLBACK_FAILURES-th trips the episode.
|
|
34
|
-
* - Not in fallback, an answering agent run: clear the running count (undefined).
|
|
35
|
-
* - Not in fallback, an inconclusive agent run: unchanged — a harness kill is not evidence the
|
|
36
|
-
* primary recovered, so the running count survives it.
|
|
37
|
-
* - In fallback, a fallback run (`probe` false): unchanged — its outcome neither returns
|
|
38
|
-
* the role to primary nor re-trips it.
|
|
39
|
-
* - In fallback, a probe run: an inconclusive probe is unchanged (retried on the next run);
|
|
40
|
-
* an answering probe clears the episode; a provider-class failed probe doubles the cooldown
|
|
41
|
-
* (capped) and schedules the next one.
|
|
42
|
-
*/
|
|
43
|
-
export function recordModelFallback(state, outcome) {
|
|
44
|
-
const { verdict, probe, now, reason } = outcome;
|
|
45
|
-
if (state !== undefined && state.since > 0) {
|
|
46
|
-
if (!probe)
|
|
47
|
-
return state;
|
|
48
|
-
if (verdict === "inconclusive")
|
|
49
|
-
return state;
|
|
50
|
-
if (verdict === "answered")
|
|
51
|
-
return undefined;
|
|
52
|
-
const cooldownMs = Math.min(state.cooldownMs * 2, MODEL_FALLBACK_MAX_COOLDOWN_MS);
|
|
53
|
-
return { ...state, cooldownMs, probeAt: now + cooldownMs, reason: reason || state.reason };
|
|
54
|
-
}
|
|
55
|
-
if (verdict === "inconclusive")
|
|
56
|
-
return state;
|
|
57
|
-
if (verdict === "answered")
|
|
58
|
-
return undefined;
|
|
59
|
-
const failures = (state?.failures ?? 0) + 1;
|
|
60
|
-
if (failures >= MODEL_FALLBACK_FAILURES) {
|
|
61
|
-
return {
|
|
62
|
-
failures: 0,
|
|
63
|
-
since: now,
|
|
64
|
-
probeAt: now + MODEL_FALLBACK_COOLDOWN_MS,
|
|
65
|
-
cooldownMs: MODEL_FALLBACK_COOLDOWN_MS,
|
|
66
|
-
reason: reason || "backend failure",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return { failures, since: 0, probeAt: 0, cooldownMs: 0, reason };
|
|
70
|
-
}
|
|
71
|
-
/** The reason label one agent run's provider evidence yields: "rate-limit" for a 429, else the
|
|
72
|
-
* backend-failure kind. Exported so the runner's evidence assembly and tests share one
|
|
73
|
-
* spelling. */
|
|
74
|
-
export function providerFailureReason(rateLimit, kind) {
|
|
75
|
-
return rateLimit ? "rate-limit" : (kind ?? "backend failure");
|
|
76
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
2
|
-
import { modelFallbackVerdict, providerFailureReason, recordModelFallback } from "./model-fallback.js";
|
|
3
|
-
/** Fold one AUTHORING agent run's verdict into the role's model-fallback state
|
|
4
|
-
* (src/runner/model-fallback.ts) and emit the episode's started/ended events. Only the agent run
|
|
5
|
-
* that actually happened is evidence: a run that never invoked pi (a skipped director
|
|
6
|
-
* inbox, a recovered leftover, a merge-side `main_red` pin) returns from runOnce before this is
|
|
7
|
-
* reached, and modelFallbackVerdict reports an aborted/killed/timed-out agent run as
|
|
8
|
-
* inconclusive — so neither can end an episode without the primary having answered.
|
|
9
|
-
* A role whose tier resolves to no fallback pair never trips, and an episode persisted from a
|
|
10
|
-
* config that HAD a pair is cleared when a live edit removes it (or sets it to "pause"), so a
|
|
11
|
-
* stale "on fallback" mark cannot outlive the config that created it. Persists on any state
|
|
12
|
-
* change; the run's own finalize save carries it too. */
|
|
13
|
-
export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
|
|
14
|
-
const s = ctx.state;
|
|
15
|
-
const prior = s.modelFallback;
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const verdict = modelFallbackVerdict(pi);
|
|
18
|
-
const reason = providerFailureReason(pi.transientRateLimit, pi.backendKind);
|
|
19
|
-
const wasIn = prior !== undefined && prior.since > 0;
|
|
20
|
-
if (fallbackCtx.fallback === null) {
|
|
21
|
-
// No fallback pair resolves any more: the feature is off for this role. End a persisted
|
|
22
|
-
// episode (the primary just ran, so this run IS the return), and drop any pre-trip count
|
|
23
|
-
// so a later config starts from zero. A killed/aborted/timed-out agent run is no verdict, so it
|
|
24
|
-
// leaves the episode in place exactly as it does on a configured fallback.
|
|
25
|
-
if (prior === undefined)
|
|
26
|
-
return;
|
|
27
|
-
if (wasIn && verdict === "inconclusive")
|
|
28
|
-
return;
|
|
29
|
-
emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
|
|
30
|
-
s.modelFallback = undefined;
|
|
31
|
-
ctx.save();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const next = recordModelFallback(prior, { verdict, probe: fallbackCtx.probe, now, reason });
|
|
35
|
-
const willBeIn = next !== undefined && next.since > 0;
|
|
36
|
-
if (!wasIn && willBeIn) {
|
|
37
|
-
logEventBestEffort(ctx.root, {
|
|
38
|
-
source: ctx.role,
|
|
39
|
-
type: "model_fallback_started",
|
|
40
|
-
provider: fallbackCtx.fallback.provider,
|
|
41
|
-
model: fallbackCtx.fallback.model,
|
|
42
|
-
reason: next?.reason,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
else if (wasIn && !willBeIn && prior !== undefined) {
|
|
46
|
-
emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
|
|
47
|
-
}
|
|
48
|
-
if (next !== prior) {
|
|
49
|
-
s.modelFallback = next;
|
|
50
|
-
ctx.save();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** Log the model_fallback_ended event for an episode that was active. Split out so the two
|
|
54
|
-
* ending paths — a successful probe and a config that dropped the pair — name the primary
|
|
55
|
-
* pair and duration the same way. A pre-trip state (no episode) has no ended event. */
|
|
56
|
-
function emitFallbackEnded(ctx, prior, primary, now, wasIn) {
|
|
57
|
-
if (!wasIn)
|
|
58
|
-
return;
|
|
59
|
-
logEventBestEffort(ctx.root, {
|
|
60
|
-
source: ctx.role,
|
|
61
|
-
type: "model_fallback_ended",
|
|
62
|
-
provider: primary.provider,
|
|
63
|
-
model: primary.model,
|
|
64
|
-
durationMs: now - (prior.since || now),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { loadConfigSafe } from "../../config/config.js";
|
|
2
|
-
import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
|
|
3
|
-
/** The Ctrl+Y claude subscription-window reserve editor on the Activity view's shared prompt
|
|
4
|
-
* line — the terminal twin of the dashboard Settings `Claude window reserve
|
|
5
|
-
* (backends.claude.windowReserve)` field and `tumwater config set claudeWindowReserve`. Enter
|
|
6
|
-
* writes the top-level `claudeWindowReserve`
|
|
7
|
-
* through the shared setConfigKey/unsetConfigKey family, so validateConfig's claudeWindowReserve
|
|
8
|
-
* block owns the value's truth. The team scope is the only scope, so the editor takes the line
|
|
9
|
-
* on the Activity view (view 0) alone; a History pane keeps Ctrl+Y for its per-role Summary
|
|
10
|
-
* toggle, which runs after this arm. */
|
|
11
|
-
export function createClaudeReserveEditor(session) {
|
|
12
|
-
return createTeamConfigKeyEditor(session, {
|
|
13
|
-
mode: "claudeReserve",
|
|
14
|
-
openCtrl: "y",
|
|
15
|
-
configKey: "claudeWindowReserve",
|
|
16
|
-
prefill: (s) => {
|
|
17
|
-
const existing = loadConfigSafe(s.root).config?.claudeWindowReserve;
|
|
18
|
-
return existing !== undefined ? JSON.stringify(existing) : "";
|
|
19
|
-
},
|
|
20
|
-
openFlash: "edit the team's claude subscription-window reserve (backends.claude.windowReserve, a JSON object like {\"seven_day\":0.8}; empty clears): Enter to save, Esc to cancel",
|
|
21
|
-
clearedFlash: "claude window reserve cleared",
|
|
22
|
-
setFlash: (text) => `claude window reserve set to ${text}`,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { loadConfigSafe } from "../../config/config.js";
|
|
2
|
-
import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
|
|
3
|
-
/** The Ctrl+P provider-concurrency-caps editor on the Activity view's shared prompt line — the
|
|
4
|
-
* terminal twin of the dashboard Settings `Provider concurrency caps (JSON)` field and
|
|
5
|
-
* `tumwater config set maxConcurrentPerProvider`, completing the run-concurrency knob set
|
|
6
|
-
* `tumwater doctor`'s checkProviderContention points at. Enter writes the top-level
|
|
7
|
-
* `maxConcurrentPerProvider` through the shared setConfigKey/unsetConfigKey family the CLI and
|
|
8
|
-
* the dashboard use — so setConfigKey JSON-parses the typed text and validateConfig's map block
|
|
9
|
-
* owns the value's truth — and Esc, Ctrl+P again, and Ctrl+T cancel by restoring the prompt
|
|
10
|
-
* draft. Mutually exclusive with the other editors on the one prompt line via the session's
|
|
11
|
-
* held registry. The Team scope is the only scope, so the editor takes the line on the Activity
|
|
12
|
-
* view (view 0) alone; a role's Transcript keeps Ctrl+P for its pause editor, which runs after
|
|
13
|
-
* this arm. */
|
|
14
|
-
export function createProviderCapsEditor(session) {
|
|
15
|
-
return createTeamConfigKeyEditor(session, {
|
|
16
|
-
mode: "providerCaps",
|
|
17
|
-
openCtrl: "p",
|
|
18
|
-
configKey: "maxConcurrentPerProvider",
|
|
19
|
-
prefill: (s) => {
|
|
20
|
-
const existing = loadConfigSafe(s.root).config?.maxConcurrentPerProvider;
|
|
21
|
-
return existing !== undefined ? JSON.stringify(existing) : "";
|
|
22
|
-
},
|
|
23
|
-
openFlash: "edit the team's provider concurrency caps (maxConcurrentPerProvider, a JSON object like {\"anthropic\":2}; empty clears): Enter to save, Esc to cancel",
|
|
24
|
-
clearedFlash: "provider concurrency caps cleared",
|
|
25
|
-
setFlash: (text) => `provider concurrency caps set to ${text}`,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|