tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
package/dist/src/brief/brief.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { briefCandidates } from "../paths.js";
|
|
5
4
|
import { errorMessage, truncateWithNote } from "../text/text.js";
|
|
6
5
|
/** The managed sections of the project brief (TUMWATER.md, with README.md as the
|
|
7
6
|
* compatibility path — plans/portability.md §7a/7): marker constants, the templates a fresh
|
|
8
|
-
* repo gets, and reading the project's
|
|
7
|
+
* repo gets, and reading the project's brief text back out. */
|
|
9
8
|
export const PROMPT_START = "<!-- tumwater:prompt:start -->";
|
|
10
9
|
export const PROMPT_END = "<!-- tumwater:prompt:end -->";
|
|
11
|
-
/** Cap on the
|
|
12
|
-
* exist and rides into EVERY
|
|
13
|
-
* per-
|
|
10
|
+
/** Cap on the brief text (see readBriefText). The prompt is the project's reason to
|
|
11
|
+
* exist and rides into EVERY run and director prompt, so unbounded text would be a standing
|
|
12
|
+
* per-run prefill cost — the same budget customRoles.task and roles.<id>.instructions are
|
|
14
13
|
* held to. `tumwater init` rejects an over-long prompt before it is committed (init.ts); this
|
|
15
14
|
* constant is the shared bound so the read path and the init check cannot drift. */
|
|
16
|
-
export const
|
|
15
|
+
export const BRIEF_TEXT_MAX_CHARS = 4096;
|
|
17
16
|
// The status markers stay exported too: the readme role targets the resolved brief file, not
|
|
18
17
|
// a hardcoded README.md, and doctor's brief check reports which file holds the sections. The
|
|
19
18
|
// prompt markers above stay exported because init.ts names them in its error message.
|
|
@@ -21,21 +20,21 @@ export const STATUS_START = "<!-- tumwater:status:start -->";
|
|
|
21
20
|
export const STATUS_END = "<!-- tumwater:status:end -->";
|
|
22
21
|
/** The placeholder every fresh template seeds between the status markers. A brief still
|
|
23
22
|
* carrying exactly this body (or an empty one) is "description incomplete"; once the readme
|
|
24
|
-
* role replaces it with a real status the
|
|
25
|
-
* template and
|
|
26
|
-
|
|
27
|
-
/** The two managed sections a fresh brief carries — the
|
|
23
|
+
* role replaces it with a real status the project's description is complete. Hoisted so the
|
|
24
|
+
* template and the readme role's real-status replacement cannot drift on the wording. */
|
|
25
|
+
const EMPTY_STATUS = "_No status yet. The readme role keeps this section up to date._";
|
|
26
|
+
/** The two managed sections a fresh brief carries — the brief text in its prompt
|
|
28
27
|
* markers, an empty status in its status markers — under the given H1. Both homes render
|
|
29
28
|
* the identical body (README.md for a fresh repo, TUMWATER.md for an adopted one), so the
|
|
30
29
|
* body lives here once and the two templates cannot drift on section wording or marker
|
|
31
30
|
* placement; the H1 is the only difference between them. */
|
|
32
|
-
function managedSections(heading,
|
|
31
|
+
function managedSections(heading, briefText) {
|
|
33
32
|
return `# ${heading}
|
|
34
33
|
|
|
35
34
|
## Brief
|
|
36
35
|
|
|
37
36
|
${PROMPT_START}
|
|
38
|
-
${
|
|
37
|
+
${briefText.trim()}
|
|
39
38
|
${PROMPT_END}
|
|
40
39
|
|
|
41
40
|
## Status
|
|
@@ -45,19 +44,19 @@ ${EMPTY_STATUS}
|
|
|
45
44
|
${STATUS_END}
|
|
46
45
|
`;
|
|
47
46
|
}
|
|
48
|
-
/** The README.md a fresh repo starts with: the
|
|
47
|
+
/** The README.md a fresh repo starts with: the brief text and an empty status,
|
|
49
48
|
* each in its managed section. */
|
|
50
|
-
export function readmeTemplate(projectName,
|
|
51
|
-
return managedSections(projectName,
|
|
49
|
+
export function readmeTemplate(projectName, briefText) {
|
|
50
|
+
return managedSections(projectName, briefText);
|
|
52
51
|
}
|
|
53
52
|
/** The TUMWATER.md an adopted repo gets (plans/portability.md §7a/7, written by 7b's adopt
|
|
54
53
|
* path): the same two managed sections as the README template, under a brief heading —
|
|
55
54
|
* so the resolution logic in this module is the only difference between the two homes. */
|
|
56
|
-
export function briefTemplate(projectName,
|
|
57
|
-
return managedSections(`${projectName} — project brief`,
|
|
55
|
+
export function briefTemplate(projectName, briefText) {
|
|
56
|
+
return managedSections(`${projectName} — project brief`, briefText);
|
|
58
57
|
}
|
|
59
58
|
/** True when `text` carries a well-formed managed prompt block: opening marker, then a
|
|
60
|
-
* closing one strictly after it (
|
|
59
|
+
* closing one strictly after it (readBriefText's ordering guard). */
|
|
61
60
|
function ownsBrief(text) {
|
|
62
61
|
const start = text.indexOf(PROMPT_START);
|
|
63
62
|
if (start < 0)
|
|
@@ -72,8 +71,8 @@ function ownsBrief(text) {
|
|
|
72
71
|
*
|
|
73
72
|
* A candidate that exists but cannot be read (README.md created as a directory, a
|
|
74
73
|
* permission-lost file) is an operator-visible break, not a silent skip: the brief is
|
|
75
|
-
* every
|
|
76
|
-
* could run the team on the wrong brief and throwing raw would crash every
|
|
74
|
+
* every role's reason to exist, read each run, so falling through to the next candidate
|
|
75
|
+
* could run the team on the wrong brief and throwing raw would crash every run with an
|
|
77
76
|
* errno that names neither the file nor the fix — the message below names both. */
|
|
78
77
|
function firstBriefText(root, accepts) {
|
|
79
78
|
for (const candidate of briefCandidates(root)) {
|
|
@@ -93,37 +92,18 @@ function firstBriefText(root, accepts) {
|
|
|
93
92
|
}
|
|
94
93
|
/** Which file owns the project's managed sections — the basename of the first candidate
|
|
95
94
|
* that carries a well-formed prompt block, or null when none does. Callers that only need
|
|
96
|
-
* the prompt use
|
|
95
|
+
* the prompt use readBriefText; this exists for the prompt builders (which name the
|
|
97
96
|
* actual file) and doctor's brief check. */
|
|
98
97
|
export function briefFile(root) {
|
|
99
98
|
return readBrief(root)?.file ?? null;
|
|
100
99
|
}
|
|
101
|
-
/**
|
|
102
|
-
* STATUS_START/STATUS_END pair (a malformed brief is doctor's warning, not a tight loop);
|
|
103
|
-
* otherwise true when the status body between the markers is empty or still the EMPTY_STATUS
|
|
104
|
-
* placeholder. The one machine-detectable "description not yet complete" signal: the initial
|
|
105
|
-
* prompt half of the description is always present (init rejects an empty one). One small
|
|
106
|
-
* single-file read, like readInitialPrompt. */
|
|
107
|
-
export function briefStatusIncomplete(root) {
|
|
108
|
-
const found = firstBriefText(root, ownsBrief);
|
|
109
|
-
if (!found)
|
|
110
|
-
return false;
|
|
111
|
-
const bodyStart = found.text.indexOf(STATUS_START);
|
|
112
|
-
if (bodyStart < 0)
|
|
113
|
-
return false;
|
|
114
|
-
const bodyEnd = found.text.indexOf(STATUS_END, bodyStart + STATUS_START.length);
|
|
115
|
-
if (bodyEnd < 0)
|
|
116
|
-
return false;
|
|
117
|
-
const body = found.text.slice(bodyStart + STATUS_START.length, bodyEnd).trim();
|
|
118
|
-
return body === "" || body === EMPTY_STATUS;
|
|
119
|
-
}
|
|
120
|
-
/** The project's initial prompt, extracted from the resolved brief file's managed section —
|
|
100
|
+
/** The project's brief text, extracted from the resolved brief file's managed section —
|
|
121
101
|
* TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
|
|
122
102
|
* closing marker is searched for only AFTER the opening one: an end marker mentioned in prose
|
|
123
103
|
* before the real block (e.g. README docs explaining how to edit the prompt) must not make a
|
|
124
|
-
* valid later block unreadable — every
|
|
104
|
+
* valid later block unreadable — every role would then run without its project prompt, and
|
|
125
105
|
* init's guard would reject re-init as if the section were missing. */
|
|
126
|
-
export function
|
|
106
|
+
export function readBriefText(root) {
|
|
127
107
|
return readBrief(root)?.prompt ?? "";
|
|
128
108
|
}
|
|
129
109
|
/** The full path of the file that owns the prompt block (the same candidate readBrief
|
|
@@ -135,7 +115,7 @@ export function briefPath(root) {
|
|
|
135
115
|
/** `text` with the prompt block's content replaced by `prompt` (trimmed), every byte outside
|
|
136
116
|
* the two markers kept; null when the text has no well-formed block. Finds the block by
|
|
137
117
|
* extractPrompt's rule (first start marker, then the first end marker after it). */
|
|
138
|
-
export function
|
|
118
|
+
export function replaceBriefText(text, prompt) {
|
|
139
119
|
const start = text.indexOf(PROMPT_START);
|
|
140
120
|
if (start < 0)
|
|
141
121
|
return null;
|
|
@@ -161,22 +141,22 @@ function extractPrompt(text) {
|
|
|
161
141
|
if (prompt === null)
|
|
162
142
|
return null;
|
|
163
143
|
// A hand-edited brief can carry an over-long prompt past init's length check. Truncate it
|
|
164
|
-
// for every
|
|
144
|
+
// for every run instead of injecting the whole thing (the same defensive cap readPrinciples
|
|
165
145
|
// applies, through text.ts's truncateWithNote), with a visible note so the loss is not
|
|
166
146
|
// silent. init rejects the normal path before it is committed, so this is the backstop.
|
|
167
|
-
return truncateWithNote(prompt,
|
|
147
|
+
return truncateWithNote(prompt, BRIEF_TEXT_MAX_CHARS, "brief");
|
|
168
148
|
}
|
|
169
149
|
/** The resolved brief as one value: the owning file's basename (briefFile) and its extracted
|
|
170
|
-
*
|
|
171
|
-
* asks for both on every
|
|
172
|
-
* scan and re-read the file — two whole-file reads per
|
|
173
|
-
*
|
|
150
|
+
* brief text (readBriefText) from a SINGLE firstBriefText scan. Run prompt assembly
|
|
151
|
+
* asks for both on every run, and briefFile + readBriefText each re-ran the candidate
|
|
152
|
+
* scan and re-read the file — two whole-file reads per run for one file's worth of content.
|
|
153
|
+
* readBriefText and briefFile now derive from this, so there is one read path; null when
|
|
174
154
|
* no candidate carries a well-formed prompt block (both derived values are then empty/null). */
|
|
175
155
|
export function readBrief(root) {
|
|
176
156
|
const found = firstBriefText(root, ownsBrief);
|
|
177
157
|
return found ? { file: found.file, prompt: extractPrompt(found.text) ?? "" } : null;
|
|
178
158
|
}
|
|
179
|
-
/** The brief's plain-text form: its file name, then the
|
|
159
|
+
/** The brief's plain-text form: its file name, then the brief text — what `tumwater brief`
|
|
180
160
|
* prints and the TUI's Brief tab shows, so the two cannot drift. */
|
|
181
161
|
export function renderBrief(brief) {
|
|
182
162
|
return `${brief.file}\n${brief.prompt}`;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/** The daily-cost budget: a
|
|
1
|
+
/** The daily-cost budget: a role's and the team's local-day spend and the gate those figures
|
|
2
2
|
* drive (plans/daily-cost-budget.md, plans/fallback-model.md). The circuit breaker that judges
|
|
3
3
|
* whether the gate's fallback can actually serve lives beside it in fallback-breaker.ts. Split
|
|
4
|
-
* out of
|
|
5
|
-
*
|
|
4
|
+
* out of runner-state.ts, which owns
|
|
5
|
+
* RunnerState persistence and scheduling bookkeeping: the budget is POLICY both the scheduler and
|
|
6
6
|
* every observer evaluate from the same definitions. Kept free of the scheduler and of pi's
|
|
7
7
|
* model catalog (fallback readiness arrives as plain booleans), so observers can depend on it
|
|
8
8
|
* without importing either — the one-way rule that previously kept these functions in
|
|
9
|
-
*
|
|
9
|
+
* runner-state.ts. */
|
|
10
10
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
11
|
-
import { nonNegativeNumber } from "../files/json-object.js";
|
|
11
|
+
import { nonNegativeNumber, setOwn } from "../files/json-object.js";
|
|
12
12
|
import { dayAt, dayKey } from "../text/datetime.js";
|
|
13
13
|
/** The local calendar day as YYYY-MM-DD — the same local-time convention as every other
|
|
14
|
-
* wall-clock display in the harness (
|
|
14
|
+
* wall-clock display in the harness (lastRunCell). */
|
|
15
15
|
export function todayStamp(now = Date.now()) {
|
|
16
16
|
return dayKey(now);
|
|
17
17
|
}
|
|
@@ -22,10 +22,10 @@ export function todayStamp(now = Date.now()) {
|
|
|
22
22
|
function spendNumber(value) {
|
|
23
23
|
return nonNegativeNumber(value, 0);
|
|
24
24
|
}
|
|
25
|
-
/** This
|
|
26
|
-
* stale or missing — a
|
|
25
|
+
/** This role's spend for the local day (the daily cost budget window): $0 when its stamp is
|
|
26
|
+
* stale or missing — a role that hasn't ran since yesterday reads as $0 today with no save
|
|
27
27
|
* required, and spend recorded before this field existed is unknown. A non-number,
|
|
28
|
-
* non-finite, or negative value reads as $0 too: `
|
|
28
|
+
* non-finite, or negative value reads as $0 too: `loadRunnerState` heals the field on read, but
|
|
29
29
|
* this is the budget gate's own guard, and a hand-edited or torn `dayCostUsd` (or an in-memory
|
|
30
30
|
* state) would otherwise concatenate into
|
|
31
31
|
* teamDailyCost's sum (or make every `>= cap` comparison false) and silently defeat the cap,
|
|
@@ -33,11 +33,11 @@ function spendNumber(value) {
|
|
|
33
33
|
export function dailyCost(s, now = Date.now()) {
|
|
34
34
|
return s.dayStamp === todayStamp(now) ? spendNumber(s.dayCostUsd) : 0;
|
|
35
35
|
}
|
|
36
|
-
/** Record a pi run's cost into the
|
|
37
|
-
* when `now` is on a different day than the recorded stamp the window resets first, so a
|
|
36
|
+
/** Record a pi run's cost into the role's daily window, rolling over at local midnight:
|
|
37
|
+
* when `now` is on a different day than the recorded stamp the window resets first, so a run
|
|
38
38
|
* that crosses midnight attributes its spend to the correct day. Mutates `s` in place — like
|
|
39
|
-
*
|
|
40
|
-
* authoritative across an in-flight
|
|
39
|
+
* applyRunOutcome and foldUsage's other counter updates, the caller's state object is
|
|
40
|
+
* authoritative across an in-flight run; the value persists at run end with the rest of the
|
|
41
41
|
* state. Both operands are guarded through spendNumber so a corrupt stored value or a
|
|
42
42
|
* non-finite/negative run cost cannot poison the window with a string/NaN/negative sum. */
|
|
43
43
|
export function recordDailyCost(s, usd, now = Date.now(), pair) {
|
|
@@ -53,10 +53,10 @@ export function recordDailyCost(s, usd, now = Date.now(), pair) {
|
|
|
53
53
|
if (pair !== undefined) {
|
|
54
54
|
const by = (s.dayCostByPair ??= {});
|
|
55
55
|
const sum = spendNumber(Object.hasOwn(by, pair) ? by[pair] : 0) + spendNumber(usd);
|
|
56
|
-
|
|
56
|
+
setOwn(by, pair, sum);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
/** One model pair's spend for the local day, summed over every
|
|
59
|
+
/** One model pair's spend for the local day, summed over every role's per-pair window
|
|
60
60
|
* (`dayCostByPair`): $0 for a stale day stamp or a pair no run has charged. */
|
|
61
61
|
export function pairDailyCost(states, pair, now = Date.now()) {
|
|
62
62
|
const today = todayStamp(now);
|
|
@@ -65,14 +65,24 @@ export function pairDailyCost(states, pair, now = Date.now()) {
|
|
|
65
65
|
return sum + (by !== undefined && Object.hasOwn(by, pair) ? spendNumber(by[pair]) : 0);
|
|
66
66
|
}, 0);
|
|
67
67
|
}
|
|
68
|
-
/** The team's spend for the local day: every
|
|
68
|
+
/** The team's spend for the local day: every role's daily window summed. */
|
|
69
69
|
export function teamDailyCost(states, now = Date.now()) {
|
|
70
70
|
return states.reduce((sum, s) => sum + dailyCost(s, now), 0);
|
|
71
71
|
}
|
|
72
|
+
/** The roles that spent most today, highest first (ties by role name), at most `limit`; roles
|
|
73
|
+
* with no spend today (zero or a stale day window) are left out. Each state is its own role, so
|
|
74
|
+
* `feature` and `feature-2` are listed apart. */
|
|
75
|
+
export function topDailySpenders(states, now = Date.now(), limit = 3) {
|
|
76
|
+
return states
|
|
77
|
+
.map((s) => ({ role: s.role, spentUsd: dailyCost(s, now) }))
|
|
78
|
+
.filter((r) => r.spentUsd > 0)
|
|
79
|
+
.sort((a, b) => b.spentUsd - a.spentUsd || (a.role < b.role ? -1 : a.role > b.role ? 1 : 0))
|
|
80
|
+
.slice(0, limit);
|
|
81
|
+
}
|
|
72
82
|
/** True when today's spend has reached the daily cost budget — the view is non-null, its cap is
|
|
73
83
|
* enabled (capUsd > 0; a cap of 0 disables the gate regardless of spend), and spend sits at or
|
|
74
84
|
* above it. The single definition of "the budget gate is on": the orchestrator evaluates it from
|
|
75
|
-
* live
|
|
85
|
+
* live runner states via budgetPaused, and both dashboards evaluate it from the snapshot's
|
|
76
86
|
* materialized budget field (status/status-data.ts) — which carries the object even while
|
|
77
87
|
* disabled, so the capUsd > 0 term is what keeps a disabled team out of "budget paused". The
|
|
78
88
|
* comparison cannot drift between what the scheduler enforces and what users see. */
|
|
@@ -84,7 +94,7 @@ export function budgetReached(budget) {
|
|
|
84
94
|
export const BUDGET_WARNING_FRACTION = 0.8;
|
|
85
95
|
/** True when today's spend has crossed `BUDGET_WARNING_FRACTION` of the daily cap while the
|
|
86
96
|
* budget gate is still open — the early heads-up beside budgetReached, which fires only once
|
|
87
|
-
* the cap itself is reached and the team has already stopped (or demoted) its
|
|
97
|
+
* the cap itself is reached and the team has already stopped (or demoted) its runs. Same
|
|
88
98
|
* guard terms as budgetReached (non-null view, a cap of 0 disables the budget), scaled by the
|
|
89
99
|
* warning fraction, so the two views can never disagree about whether the budget is on. */
|
|
90
100
|
export function budgetWarning(budget) {
|
|
@@ -98,7 +108,7 @@ export function budgetWarning(budget) {
|
|
|
98
108
|
export function budgetPaused(states, config, now = Date.now()) {
|
|
99
109
|
return budgetReached(budgetSpend(states, config, now));
|
|
100
110
|
}
|
|
101
|
-
/** The team's daily-budget figures from live
|
|
111
|
+
/** The team's daily-budget figures from live runner states — the exact pair the gate evaluates:
|
|
102
112
|
* today's summed spend and the configured cap (0 when disabled). budgetReached's own capUsd > 0
|
|
103
113
|
* term makes a disabled cap read unreached, so the pair needs no null ceremony. Returning the
|
|
104
114
|
* figures (not just the verdict) lets the scheduler publish WHAT it tripped on, not only THAT it
|
|
@@ -141,7 +151,7 @@ export function modelPairName(p) {
|
|
|
141
151
|
* `strong` tiers resolved to a usable free pair (resolveTierFallbacks over the full usable
|
|
142
152
|
* predicate — price AND breaker serving) — plus whether the review gate is on. The gate is
|
|
143
153
|
* `paused` when `default` resolves to pause (nothing for the team's bulk to run on), or when
|
|
144
|
-
* review is on and `strong` does (nothing could merge, so every authoring
|
|
154
|
+
* review is on and `strong` does (nothing could merge, so every authoring run is wasted work);
|
|
145
155
|
* `fallback` otherwise, with the tiers that did resolve running their pairs and the rest held
|
|
146
156
|
* per role. The single definition of the gate: the orchestrator enforces it and both dashboards
|
|
147
157
|
* display it, so what an operator sees is what the scheduler is doing. */
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The fallback's circuit breaker (BUGS.md 2026-09-20): whether the engaged free fallback can
|
|
3
2
|
* actually SERVE. Split out of budget.ts, which keeps the daily-cost figures and the gate they
|
|
4
3
|
* drive; the breaker is a self-contained pure state machine — no scheduler, no pi model
|
|
5
|
-
* catalog, no persistence — that depends on nothing but the
|
|
6
|
-
* orchestrator can hold it in memory and fold
|
|
4
|
+
* catalog, no persistence — that depends on nothing but the RunResult vocabulary, so the
|
|
5
|
+
* orchestrator can hold it in memory and fold run outcomes into it without importing the
|
|
7
6
|
* budget. The gate reads its verdict through fallbackServing (budget.ts's budgetGate). */
|
|
8
|
-
/** failureLimit 3 is a single
|
|
9
|
-
* applied team-wide: consecutive across every role, so any
|
|
10
|
-
* it — a healthy backend with one flaky
|
|
11
|
-
* first three
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
/** failureLimit 3 is a single role's failing-streak threshold (run-apply.ts ERROR_STREAK_WARN)
|
|
8
|
+
* applied team-wide: consecutive across every role, so any run that served in between resets
|
|
9
|
+
* it — a healthy backend with one flaky run never trips it, while a dead one trips on its
|
|
10
|
+
* first three runs (on 2026-09-19 the third failure landed 24 minutes into the hour; the 30
|
|
11
|
+
* runs after it were the waste). A 5-minute first cool-down because a failed probe costs one
|
|
12
|
+
* run — about half of that hour's failures came back in under 10 s, the rest ran minutes
|
|
14
13
|
* before the 400 — which is cheap next to idling until midnight; doubling to at most 30 minutes
|
|
15
14
|
* bounds how long an operator who fixed the backend waits for the team to notice (editing the
|
|
16
15
|
* cap or the pair re-trusts it at once — see rekeyFallbackBreaker). */
|
|
@@ -44,34 +43,34 @@ export function fallbackServing(b) {
|
|
|
44
43
|
return b.probeAt === null;
|
|
45
44
|
}
|
|
46
45
|
/** Half-open: the demoted fallback's cool-down has elapsed and no probe is in flight, so one role
|
|
47
|
-
*
|
|
46
|
+
* run may start on it despite the `paused` gate. */
|
|
48
47
|
export function fallbackProbeDue(b, now) {
|
|
49
48
|
return b.pair !== null && b.probeAt !== null && !b.probing && now >= b.probeAt;
|
|
50
49
|
}
|
|
51
|
-
/** The breaker once the probe
|
|
52
|
-
* back, so every other due role waits for its verdict — one
|
|
50
|
+
/** The breaker once the probe run has been admitted: probing until recordFallbackRun hears
|
|
51
|
+
* back, so every other due role waits for its verdict — one run's evidence per cool-down, not
|
|
53
52
|
* a whole maxConcurrent wave of failures. */
|
|
54
53
|
export function startFallbackProbe(b) {
|
|
55
54
|
return { ...b, probing: true };
|
|
56
55
|
}
|
|
57
56
|
/** The breaker when an admitted probe never ran — turned away at its permit by the restart
|
|
58
57
|
* hold's start gate: no evidence either way, so the claim is handed back and the probe is due
|
|
59
|
-
* again at the next poll. Without this the breaker would wait on a verdict no
|
|
58
|
+
* again at the next poll. Without this the breaker would wait on a verdict no run will ever
|
|
60
59
|
* deliver, and the demoted pause could only clear on a rekey. */
|
|
61
60
|
export function abandonFallbackProbe(b) {
|
|
62
61
|
return { ...b, probing: false };
|
|
63
62
|
}
|
|
64
|
-
/** What one role
|
|
63
|
+
/** What one role run that ran on the fallback says about its backend. `failed` is `error`
|
|
65
64
|
* alone: every way a backend cannot serve — an HTTP 4xx/5xx like oMLX's prefill guard, a
|
|
66
|
-
* refused connection, a request that never returns (the
|
|
65
|
+
* refused connection, a request that never returns (the run timeout) — ends a run as `error`,
|
|
67
66
|
* and 33 of 33 did on 2026-09-19. (An `error` can also be local, e.g. a worktree that will not
|
|
68
67
|
* reset; failureLimit of those in a row team-wide is still a team that cannot work, and the
|
|
69
68
|
* probe wins the pause back.) `served` is a result only a model reply produces: a change bound
|
|
70
69
|
* for merge (`queued`, `changed`), an explicit decision (`no_change`, `refused`), or a
|
|
71
70
|
* reviewer's rejection. Everything else is no evidence: harness and operator kills (`aborted`,
|
|
72
|
-
* `user_aborted`),
|
|
71
|
+
* `user_aborted`), runs that never reached the model (`skipped`, `main_red`), a quiet kill
|
|
73
72
|
* (the model had answered before the stall — a tool call or the stream stopped), and the
|
|
74
|
-
* merge results (`merge_conflict`, `merge_blocked`, `review_error`) — a
|
|
73
|
+
* merge results (`merge_conflict`, `merge_blocked`, `review_error`) — a run no longer
|
|
75
74
|
* returns them since leftover recovery queues its pin instead of merging it, and they would
|
|
76
75
|
* not say whether the backend was involved. */
|
|
77
76
|
export function fallbackEvidence(result) {
|
|
@@ -88,15 +87,15 @@ export function fallbackEvidence(result) {
|
|
|
88
87
|
return "none";
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
|
-
/** Fold one finished role
|
|
92
|
-
* the
|
|
93
|
-
* and `probe` says whether the orchestrator admitted it as the half-open probe. A served
|
|
90
|
+
/** Fold one finished role run that ran on the fallback into the breaker. `ranOn` is the breaker as
|
|
91
|
+
* the run started under it — evidence about a subject that is no longer engaged is dropped —
|
|
92
|
+
* and `probe` says whether the orchestrator admitted it as the half-open probe. A served run
|
|
94
93
|
* closes the breaker from any state (the backend demonstrably works). A failed probe re-opens it
|
|
95
|
-
* with the cool-down doubled up to maxCooldownMs; a failed
|
|
94
|
+
* with the cool-down doubled up to maxCooldownMs; a failed run while closed counts toward
|
|
96
95
|
* failureLimit and trips the breaker at it; a failed straggler that started before the trip leaves
|
|
97
96
|
* the running cool-down alone. A probe with no evidence frees the probe slot, so the next due role
|
|
98
97
|
* probes instead. */
|
|
99
|
-
export function
|
|
98
|
+
export function recordFallbackRun(b, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
|
|
100
99
|
if (ranOn.pair === null || ranOn.pair !== b.pair || ranOn.capUsd !== b.capUsd)
|
|
101
100
|
return b;
|
|
102
101
|
const isProbe = probe && b.probing;
|
|
@@ -143,7 +142,7 @@ export function fallbackServingPair(map, pair) {
|
|
|
143
142
|
}
|
|
144
143
|
/** The pair whose half-open window is due, or null when none is. Several pairs can carry a
|
|
145
144
|
* demotion at once (one per resolved tier), so the walk returns the first due entry in map
|
|
146
|
-
* order and the caller probes that single pair this
|
|
145
|
+
* order and the caller probes that single pair this run. */
|
|
147
146
|
export function fallbackProbeDuePair(map, now) {
|
|
148
147
|
for (const [pair, b] of Object.entries(map)) {
|
|
149
148
|
if (fallbackProbeDue(b, now))
|
|
@@ -153,7 +152,7 @@ export function fallbackProbeDuePair(map, now) {
|
|
|
153
152
|
}
|
|
154
153
|
/** Apply `f` to the pair's entry when the map holds one, else return the map unchanged — the
|
|
155
154
|
* shape shared by the map-scoped breaker transitions below (claim/hand back a probe, fold a
|
|
156
|
-
*
|
|
155
|
+
* run): a pair the resolution dropped has no judgment to touch. */
|
|
157
156
|
function withPair(map, pair, f) {
|
|
158
157
|
const b = map[pair];
|
|
159
158
|
return b === undefined ? map : { ...map, [pair]: f(b) };
|
|
@@ -168,9 +167,9 @@ export function startFallbackProbeAt(map, pair) {
|
|
|
168
167
|
export function abandonFallbackProbeAt(map, pair) {
|
|
169
168
|
return withPair(map, pair, abandonFallbackProbe);
|
|
170
169
|
}
|
|
171
|
-
/** Fold one finished role
|
|
170
|
+
/** Fold one finished role run into the pair named `pair`'s entry (recordFallbackRun's
|
|
172
171
|
* semantics, scoped to the map): an absent entry has no judgment to fold into and is left
|
|
173
172
|
* alone. */
|
|
174
|
-
export function
|
|
175
|
-
return withPair(map, pair, (b) =>
|
|
173
|
+
export function recordFallbackRunAt(map, pair, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
|
|
174
|
+
return withPair(map, pair, (b) => recordFallbackRun(b, ranOn, result, probe, now, policy));
|
|
176
175
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { isJsonObject } from "../files/json-object.js";
|
|
5
4
|
import { readJsonFile } from "../files/json-files.js";
|
|
6
|
-
import { walkUp } from "../files/files.js";
|
|
5
|
+
import { isDirectory, statOrNull, walkUp } from "../files/files.js";
|
|
7
6
|
import { isNonBlankString } from "../text/text.js";
|
|
8
|
-
/** Hard cap on one build check run — a hung script (watch mode) must not wedge the
|
|
7
|
+
/** Hard cap on one build check run — a hung script (watch mode) must not wedge the run, and
|
|
9
8
|
* a timeout is environmental, never fail-closed. The default `check.timeoutSeconds` resolves
|
|
10
9
|
* to this (via ms in detectBuildCheck); runBuildCheck uses it as its default timeout
|
|
11
10
|
* parameter so tests can shorten it. */
|
|
@@ -18,13 +17,7 @@ const WALK_UP_LEVELS = 5;
|
|
|
18
17
|
/** True when `dir` holds both a package.json and a node_modules/ directory — the structural
|
|
19
18
|
* signature of an installed JS project root. */
|
|
20
19
|
function hasInstall(dir) {
|
|
21
|
-
|
|
22
|
-
fs.statSync(path.join(dir, "package.json"));
|
|
23
|
-
return fs.statSync(path.join(dir, "node_modules")).isDirectory();
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
20
|
+
return statOrNull(path.join(dir, "package.json")) !== null && isDirectory(path.join(dir, "node_modules"));
|
|
28
21
|
}
|
|
29
22
|
/** Read the check script from `dir`'s package.json: prefer test — npm convention makes
|
|
30
23
|
* `npm test` the canonical verify command — then typecheck, then build; null when none is
|
|
@@ -95,15 +88,30 @@ export function detectBuildCheck(startDir, config, maxLevels = WALK_UP_LEVELS) {
|
|
|
95
88
|
return found !== null && found.kind === "npm" && configuredMs !== undefined ? { ...found, timeoutMs: configuredMs } : found;
|
|
96
89
|
}
|
|
97
90
|
/** The configured per-change gate command (`check.gateCommand`, PLANS.md Land-queue speed 3e) when
|
|
98
|
-
* it is a non-blank string, else undefined — off. Opt-in:
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* merge-to-main.ts's verifyMerge (which must then never treat the gate's green as the
|
|
102
|
-
* check's), so the two cannot disagree about whether it is on. */
|
|
91
|
+
* it is a non-blank string, else undefined — off. Opt-in: when no `mergeCommand` is declared, a
|
|
92
|
+
* project that sets one merges on it, not the full check (mergeCommandOf's fallback, PLANS.md
|
|
93
|
+
* "Fast merge" "Defaults"). One reading shared by runScopedBuildCheck (which runs it at scope
|
|
94
|
+
* "gate") and merge-to-main.ts's verifyMerge (which must then never treat the gate's green as the
|
|
95
|
+
* full check's), so the two cannot disagree about whether it is on. */
|
|
103
96
|
export function gateCommandOf(config) {
|
|
104
97
|
const g = config?.check?.gateCommand;
|
|
105
98
|
return isNonBlankString(g) ? g : undefined;
|
|
106
99
|
}
|
|
100
|
+
/** The configured in-lock merge command (`check.mergeCommand`, PLANS.md "Fast merge" part 6/14)
|
|
101
|
+
* when it is a non-blank string, else undefined — off. An absent `mergeCommand` falls back to
|
|
102
|
+
* the gate command (`gateCommandOf`, PLANS.md "Fast merge" "Defaults"): a project that declared
|
|
103
|
+
* a `check.gateCommand` merges on it. With it set, the merge and batch scopes run it instead of
|
|
104
|
+
* the full check, and main's full-check verdict comes from the main watch. A blank string
|
|
105
|
+
* (`""`) is the explicit opt-out that keeps the full check in the merge lock even when a gate
|
|
106
|
+
* command is set. One reading shared by runScopedBuildCheck, the merge paths and the main-red
|
|
107
|
+
* verdict reads, so they cannot disagree about whether it is on. */
|
|
108
|
+
export function mergeCommandOf(config) {
|
|
109
|
+
const c = config?.check;
|
|
110
|
+
const m = c?.mergeCommand;
|
|
111
|
+
if (m === undefined)
|
|
112
|
+
return gateCommandOf(config);
|
|
113
|
+
return isNonBlankString(m) ? m : undefined;
|
|
114
|
+
}
|
|
107
115
|
/** Resolve `node_modules/<rel>` by walking UP from `startDir` — the same climb detectBuildCheck
|
|
108
116
|
* makes, and the same one npm's run-script PATH walk makes: a tumwater worktree has no install
|
|
109
117
|
* of its own (node_modules is gitignored, so it exists only where someone ran npm install), and
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The presentation half of the build check: the scope vocabulary, the environmental-skip
|
|
3
2
|
* warning's wording, and the build_check event's shape. Split out of build-check.ts — which
|
|
4
3
|
* keeps the deterministic execution and classification (detect → spawn → classify) — because
|
|
@@ -8,21 +7,27 @@
|
|
|
8
7
|
* running a check. This module imports only types from build-check.js, so no runtime cycle.
|
|
9
8
|
* runScopedBuildCheck (build-check-scoped.ts) is the only caller that both runs a check and
|
|
10
9
|
* uses this wording; the baseline surfaces use the event/warning helpers alone. */
|
|
10
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
11
11
|
/** Does this build_check scope stand for a full-suite verdict — merge, batch, or the red-main
|
|
12
12
|
* baseline? A gate check can run a subset of the tree, so a scope outside this set is never a
|
|
13
13
|
* full-suite check. The one vocabulary the three predicates that recognize a full-suite check
|
|
14
14
|
* select on — isPassingFullCheck, isRedFullCheck, and the status polls' newest-merge-check
|
|
15
15
|
* scan (status-polls.ts) — so they cannot drift on which scopes count. The deadline-killed
|
|
16
16
|
* failure costing in time-spend.ts is a narrower pair (merge/batch only, with a status and a
|
|
17
|
-
* timeout required), so it keeps its own check.
|
|
18
|
-
|
|
17
|
+
* timeout required), so it keeps its own check. `full` is the event's `full` field: false
|
|
18
|
+
* (a check.mergeCommand run) is never a full-suite check. */
|
|
19
|
+
export function isFullCheckScope(scope, full) {
|
|
20
|
+
// A merge or batch run of check.mergeCommand carries `full: false` (buildCheckEvent): it ran
|
|
21
|
+
// the cheaper command, so it is no full-suite verdict whatever its scope.
|
|
22
|
+
if (full === false)
|
|
23
|
+
return false;
|
|
19
24
|
return scope === "merge" || scope === "batch" || scope === "baseline";
|
|
20
25
|
}
|
|
21
26
|
/** Is this a passing build_check over the full suite? The scopes doctor's check row counts —
|
|
22
27
|
* merge, batch and the red-main baseline — are isFullCheckScope's set; a gate check can run a
|
|
23
28
|
* subset, so it is left out. */
|
|
24
29
|
export function isPassingFullCheck(ev) {
|
|
25
|
-
return ev.type === "build_check" && ev.status === "passed" && isFullCheckScope(ev.scope);
|
|
30
|
+
return ev.type === "build_check" && ev.status === "passed" && isFullCheckScope(ev.scope, ev.full);
|
|
26
31
|
}
|
|
27
32
|
/** The red sibling of isPassingFullCheck: a failed build_check over the same three scopes
|
|
28
33
|
* (isFullCheckScope's set). A `skipped` check made no verdict about the tree, and a merge-scope
|
|
@@ -30,7 +35,7 @@ export function isPassingFullCheck(ev) {
|
|
|
30
35
|
* alone is the rule. The two predicates are mutually exclusive on `status`, so a fold branch on
|
|
31
36
|
* them never sees the other's events. */
|
|
32
37
|
export function isRedFullCheck(ev) {
|
|
33
|
-
return ev.type === "build_check" && ev.status === "failed" && isFullCheckScope(ev.scope);
|
|
38
|
+
return ev.type === "build_check" && ev.status === "failed" && isFullCheckScope(ev.scope, ev.full);
|
|
34
39
|
}
|
|
35
40
|
/** How late a check's deadline timer may fire before the timeout's wording stops naming the
|
|
36
41
|
* configured bound alone. Normal timer lag is milliseconds; past this the warning and the
|
|
@@ -236,11 +241,14 @@ function buildCheckRunFields(outcome) {
|
|
|
236
241
|
* runScopedBuildCheck's two priced events in build-check-scoped.ts (the killed retry and the
|
|
237
242
|
* final verdict) and the red-main baseline loggers (main-red.ts's baselineCheckLogger,
|
|
238
243
|
* redeploy.ts's redeploy mirror), which all paid the run whose timings the event carries. */
|
|
239
|
-
export function buildCheckEvent(source, scope, outcome, durationMs
|
|
244
|
+
export function buildCheckEvent(source, scope, outcome, durationMs,
|
|
245
|
+
/** True for a merge/batch run of check.mergeCommand: the event then carries `full: false`. */
|
|
246
|
+
cheaper = false) {
|
|
240
247
|
return {
|
|
241
248
|
source,
|
|
242
249
|
type: "build_check",
|
|
243
250
|
scope,
|
|
251
|
+
...(cheaper ? { full: false } : {}),
|
|
244
252
|
status: outcome.status,
|
|
245
253
|
script: outcome.script,
|
|
246
254
|
durationMs,
|
|
@@ -259,6 +267,17 @@ export function buildCheckEvent(source, scope, outcome, durationMs) {
|
|
|
259
267
|
: {}),
|
|
260
268
|
};
|
|
261
269
|
}
|
|
270
|
+
/** A scoped check that waited at least this long for a process-wide check permit is worth an
|
|
271
|
+
* event: it is the operator's signal that `maxConcurrentChecks` is too small for the team. */
|
|
272
|
+
export const CHECK_WAIT_EVENT_MS = 30_000;
|
|
273
|
+
/** Log one `check_wait` when a scoped check waited at least CHECK_WAIT_EVENT_MS for a check
|
|
274
|
+
* permit; a shorter wait is ordinary queueing and stays silent. The one home of the event's
|
|
275
|
+
* shape, beside buildCheckEvent. Best-effort: an unwritable feed must not fail the check. */
|
|
276
|
+
export function logCheckWaitEvent(root, role, scope, waitedMs, parked) {
|
|
277
|
+
if (waitedMs < CHECK_WAIT_EVENT_MS)
|
|
278
|
+
return;
|
|
279
|
+
logEventBestEffort(root, { source: role, type: "check_wait", role, scope, waitedMs, ...parked });
|
|
280
|
+
}
|
|
262
281
|
/** The onRun hook that logs one baseline check's build_check event through `log` under `source`:
|
|
263
282
|
* main-red.ts's gate logger (logEventBestEffort) and redeploy.ts's green-check and witness hooks
|
|
264
283
|
* all turn the same run shape into the same baseline event, so that mapping lives here once
|