tumwater 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +55 -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 +6 -4
- 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 +404 -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 +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -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 +264 -0
- package/dist/src/backlog/backlog-release.js +134 -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-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -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 +65 -3
- 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 +181 -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 +183 -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 +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- 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 +16 -1
- package/dist/src/config/config-fallbacks.js +94 -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 +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- 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 +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- 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 +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- 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 +89 -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 +54 -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 +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- 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 +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- 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 +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -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 +28 -17
- 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 +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- 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 +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -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 +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -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 +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -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 +126 -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 +26 -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 +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -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 +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- 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 +180 -401
- 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 +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -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} +122 -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} +170 -83
- 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} +105 -57
- 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 +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -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} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- 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 +49 -17
- 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 +43 -28
- package/dist/src/status/status-roles.js +50 -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 +7 -8
- 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 +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- 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 +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -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 +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- 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 +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -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 +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -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 +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -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 +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- 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
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { eventRole, eventUsage,
|
|
1
|
+
import { eventRole, eventUsage, runSpanMs, runStartMap } from "../events/event-read.js";
|
|
2
2
|
import { errorTextOrPlaceholder, normalizeClusterKey, poolTimeoutKey, sortedRoles, truncateExample } from "./failure-cluster.js";
|
|
3
|
-
import { resolveQueuedResult, bucketMergeEvents } from "../history/history-
|
|
3
|
+
import { resolveQueuedResult, bucketMergeEvents } from "../history/history-queued.js";
|
|
4
4
|
import { nonNegativeNumber, stringList } from "../files/json-object.js";
|
|
5
5
|
import { getOrCreate, groupBy, rankByCount } from "../collections.js";
|
|
6
6
|
/** How the Outcome table's results collapse for costing:
|
|
7
|
-
* "merged" made progress, "no_change" spent a
|
|
7
|
+
* "merged" made progress, "no_change" spent a run and merged nothing, and every remaining
|
|
8
8
|
* result is "error-class" — it burned agent time without merge, whether the cause was a
|
|
9
9
|
* hard failure or the review gate. Typed as a full Record so a result added to
|
|
10
|
-
* src/
|
|
10
|
+
* src/run/run-outcome.ts and forgotten here is a compile error, like RESULT_ORDER in
|
|
11
11
|
* src/failure/failure-render.ts. */
|
|
12
12
|
export const OUTCOME_CLASS = {
|
|
13
13
|
changed: "merged",
|
|
@@ -35,7 +35,7 @@ const LOSS_TOP = 5;
|
|
|
35
35
|
/** The raw results whose loss attributes to a cluster — an error, an abort, a quiet kill, or
|
|
36
36
|
* a red-main skip, each carrying its own message on the run_end (main_red's is the
|
|
37
37
|
* redMainMessage the error-clusters section already itemizes, BUGS.md 2026-10-07). A queued
|
|
38
|
-
*
|
|
38
|
+
* run's merge outcomes are owned by the joins below instead: review_error through its
|
|
39
39
|
* matching `review_failed` message, review-rejected through its review reasons, and
|
|
40
40
|
* merge_conflict/merge_blocked by the result itself (BUGS.md 2026-10-10). (A raw
|
|
41
41
|
* `user_aborted` run_end is also error-class but is not in this set, so it stays out of the
|
|
@@ -45,8 +45,8 @@ const CLUSTERED_RESULTS = new Set(["error", "aborted", "quiet_killed", "main_red
|
|
|
45
45
|
* other can occupy. Shared by the review-rejected and review_error cause joins, so the two
|
|
46
46
|
* agree on which event belongs to the attempt (BUGS.md 2026-10-10: the window, not the pin's
|
|
47
47
|
* sha, is the match, since every verification rebases its pin before the gate). */
|
|
48
|
-
function newestAttemptEvent(
|
|
49
|
-
return (
|
|
48
|
+
function newestAttemptEvent(eventsByRunner, role, attempt) {
|
|
49
|
+
return (eventsByRunner.get(role) ?? [])
|
|
50
50
|
.filter((e) => e.ts >= attempt.pin.ts && e.ts <= attempt.outcome.ts)
|
|
51
51
|
.at(-1);
|
|
52
52
|
}
|
|
@@ -64,14 +64,14 @@ function pooledClusterCause(source) {
|
|
|
64
64
|
* written since 2026-09-29 does), else the start→end pairing over `starts` — the same
|
|
65
65
|
* fallback history-data.ts renders, so old events read their span the one way it can still be
|
|
66
66
|
* known. 0 when neither source has the start (rotation cut it); the fold prices no time it
|
|
67
|
-
* cannot attest but still counts the
|
|
68
|
-
function
|
|
67
|
+
* cannot attest but still counts the run. */
|
|
68
|
+
function runDurationMs(ev, starts) {
|
|
69
69
|
const own = nonNegativeNumber(ev.durationMs, null);
|
|
70
70
|
if (own !== null)
|
|
71
71
|
return own;
|
|
72
|
-
return
|
|
72
|
+
return runSpanMs(ev, starts) ?? 0;
|
|
73
73
|
}
|
|
74
|
-
/** Add one
|
|
74
|
+
/** Add one run to a {runs, ms, costUsd} accumulator — the single home of the fold shared
|
|
75
75
|
* by the time-and-spend table's outcome cells (SpendCell) and the loss-cause drafts
|
|
76
76
|
* (LossDraft), so the two tallies cannot drift apart in what they count or price. The
|
|
77
77
|
* caller passes one event's already-computed durationMs and usage, so folding an event
|
|
@@ -82,48 +82,48 @@ function tally(target, durationMs, usage) {
|
|
|
82
82
|
target.costUsd += usage.costUsd;
|
|
83
83
|
}
|
|
84
84
|
/** Fold the window's run_ends into the time-and-spend table and the loss ranking. Pairing
|
|
85
|
-
* runs over BOTH windows' events (the caller passes the whole read), so a
|
|
85
|
+
* runs over BOTH windows' events (the caller passes the whole read), so a run that opened
|
|
86
86
|
* in the preceding day and ended in the current one still gets its span. The merge-outcome
|
|
87
87
|
* join runs over that same whole read too (BUGS.md 2026-09-30): a queued run_end resolves
|
|
88
88
|
* through its own merge_queued pin to the later merge_done/merge_failed outcome for the same
|
|
89
|
-
*
|
|
89
|
+
* role+sha — history-data.ts's exact join — so an authoring run whose merge was rejected
|
|
90
90
|
* prices into the error-class cell its contract names instead of the merged cell its
|
|
91
91
|
* transient queued result suggested. A leftover-recovery run resolves to the failed attempt it
|
|
92
92
|
* re-queued (the history row's rule), so its near-zero span counts under that failure — a
|
|
93
93
|
* review timeout's cluster, say — rather than adding a run to the change's final
|
|
94
94
|
* review-rejected cause once per re-queue. A merge still in the pipeline, or an outcome event
|
|
95
95
|
* matching no pin, keeps the queued→merged reading: the conservative fallback. */
|
|
96
|
-
export function timeAndSpend(
|
|
97
|
-
const starts =
|
|
96
|
+
export function timeAndSpend(runEvents, allEvents) {
|
|
97
|
+
const starts = runStartMap(allEvents);
|
|
98
98
|
const emptyCell = () => ({ runs: 0, ms: 0, costUsd: 0 });
|
|
99
99
|
const byRole = new Map();
|
|
100
100
|
const losses = new Map();
|
|
101
|
-
// The merge evidence rides history-
|
|
101
|
+
// The merge evidence rides history-queued.ts's bucketMergeEvents — the same buckets
|
|
102
102
|
// history's rows read — so the digest's join cannot drift from the rows' join.
|
|
103
|
-
const {
|
|
104
|
-
const
|
|
103
|
+
const { mergeQueuedByRunner, outcomeByRunner } = bucketMergeEvents(allEvents);
|
|
104
|
+
const rejectedByRunner = groupBy(allEvents.filter((e) => e.type === "review_rejected"), (e) => e.source);
|
|
105
105
|
// A review_error merge kept its pin but left no message on its own merge_failed; the
|
|
106
|
-
// matching review_failed event for the same
|
|
106
|
+
// matching review_failed event for the same role+sha carries it, so the span prices under
|
|
107
107
|
// the same cluster key the digest's review-failure section already counts.
|
|
108
|
-
const
|
|
109
|
-
const
|
|
108
|
+
const reviewFailedByRunner = groupBy(allEvents.filter((e) => e.type === "review_failed"), (e) => e.source);
|
|
109
|
+
const deadlineKilledByRunner = groupBy(allEvents.filter((e) => e.type === "build_check" &&
|
|
110
110
|
e.status === "failed" &&
|
|
111
111
|
(e.scope === "merge" || e.scope === "batch") &&
|
|
112
112
|
typeof e.timeoutMs === "number"), (e) => e.source);
|
|
113
113
|
// Queued run_ends resolve newest-first so each claims the newest pin at or before its end
|
|
114
|
-
// that no newer
|
|
115
|
-
// history rows.
|
|
114
|
+
// that no newer run has claimed — resolveQueuedResult's exact claim rule, shared with the
|
|
115
|
+
// history rows. runEvents is oldest-first, hence the backward walk.
|
|
116
116
|
const claimTop = new Map();
|
|
117
117
|
const resolved = new Map();
|
|
118
|
-
for (let i =
|
|
119
|
-
const ev =
|
|
118
|
+
for (let i = runEvents.length - 1; i >= 0; i--) {
|
|
119
|
+
const ev = runEvents[i];
|
|
120
120
|
if (ev.result !== "queued")
|
|
121
121
|
continue;
|
|
122
|
-
const joined = resolveQueuedResult(ev,
|
|
122
|
+
const joined = resolveQueuedResult(ev, mergeQueuedByRunner, outcomeByRunner, claimTop, new Map(), null);
|
|
123
123
|
if (joined !== null)
|
|
124
124
|
resolved.set(ev, joined);
|
|
125
125
|
}
|
|
126
|
-
for (const ev of
|
|
126
|
+
for (const ev of runEvents) {
|
|
127
127
|
let cls = OUTCOME_CLASS[ev.result];
|
|
128
128
|
if (cls === undefined)
|
|
129
129
|
continue; // An unknown result is tallied in the Outcome table; costing it would need a class first.
|
|
@@ -138,7 +138,7 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
138
138
|
}
|
|
139
139
|
const role = eventRole(ev);
|
|
140
140
|
const usage = eventUsage(ev);
|
|
141
|
-
const durationMs =
|
|
141
|
+
const durationMs = runDurationMs(ev, starts);
|
|
142
142
|
const row = getOrCreate(byRole, role, () => ({
|
|
143
143
|
merged: emptyCell(),
|
|
144
144
|
no_change: emptyCell(),
|
|
@@ -155,9 +155,9 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
155
155
|
if (ev.result === "refused") {
|
|
156
156
|
// A refusal is a deliberate objection that blocks its entry, not quiet idleness — the
|
|
157
157
|
// reason live in the event + lastSummary (src/verdict/refusal.ts). Rank it as its own
|
|
158
|
-
// per-role cause so the digest surfaces the objection instead of pricing the
|
|
158
|
+
// per-role cause so the digest surfaces the objection instead of pricing the run
|
|
159
159
|
// exactly like a silently idle one; the branch sits BEFORE the no_change one because a
|
|
160
|
-
// refused
|
|
160
|
+
// refused run is also no_change-class in the outcome table. The example rides the
|
|
161
161
|
// refusal's summary the way a review-rejected cause rides its reason.
|
|
162
162
|
key = `refused\u0000${role}`;
|
|
163
163
|
kind = "refused";
|
|
@@ -169,8 +169,8 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
169
169
|
}
|
|
170
170
|
else if (CLUSTERED_RESULTS.has(String(ev.result))) {
|
|
171
171
|
// A clustered result with no error text still owns its time: an abort's run_end never
|
|
172
|
-
// carries one (
|
|
173
|
-
// and a malformed error
|
|
172
|
+
// carries one (planRunStart clears s.lastError, run-finalize logs error: undefined),
|
|
173
|
+
// and a malformed error run can lose it too, so both take the same placeholder cause the
|
|
174
174
|
// error-clusters section already itemizes rather than dropping the span itemized nowhere.
|
|
175
175
|
({ key, example } = pooledClusterCause(ev.error));
|
|
176
176
|
}
|
|
@@ -182,7 +182,7 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
182
182
|
// `merge_queued` keeps the authored one — exact-sha equality names no reason for a rebased
|
|
183
183
|
// rejection (BUGS.md 2026-10-10). The rejection for this attempt is the one inside the
|
|
184
184
|
// claimed pin and its outcome, the window no other attempt can occupy.
|
|
185
|
-
const rej = newestAttemptEvent(
|
|
185
|
+
const rej = newestAttemptEvent(rejectedByRunner, role, resolvedOutcome);
|
|
186
186
|
const reasons = stringList(rej?.reasons);
|
|
187
187
|
example = reasons[0] !== undefined ? truncateExample(reasons[0]) : "";
|
|
188
188
|
}
|
|
@@ -198,7 +198,7 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
198
198
|
// keeps the authored one — exact-sha equality missed every rebased verification (BUGS.md
|
|
199
199
|
// 2026-10-10). The review failure for this attempt is the one that falls inside the
|
|
200
200
|
// claimed pin and its outcome, the window no other attempt can occupy.
|
|
201
|
-
const failed = newestAttemptEvent(
|
|
201
|
+
const failed = newestAttemptEvent(reviewFailedByRunner, role, resolvedOutcome);
|
|
202
202
|
({ key, example } = pooledClusterCause(failed?.message));
|
|
203
203
|
}
|
|
204
204
|
else if (ev.result === "queued" &&
|
|
@@ -214,7 +214,7 @@ export function timeAndSpend(tickEvents, allEvents) {
|
|
|
214
214
|
example = errorTextOrPlaceholder(undefined);
|
|
215
215
|
// A merge-scope check the harness killed at its own deadline is the team's load, not a
|
|
216
216
|
// verdict on the change: split it into its own cause so the ranking names it.
|
|
217
|
-
const killed = newestAttemptEvent(
|
|
217
|
+
const killed = newestAttemptEvent(deadlineKilledByRunner, role, resolvedOutcome);
|
|
218
218
|
if (kind === "merge-blocked" && killed) {
|
|
219
219
|
key = `merge_blocked_deadline\u0000${role}`;
|
|
220
220
|
example = `${String(killed.scope)} build check killed at its ${Number(killed.timeoutMs) / 1000}s deadline (tumwater's own timeout, not a test failure)`;
|
|
@@ -44,6 +44,23 @@ export function queueFileStamp(name) {
|
|
|
44
44
|
const m = /^(\d{13})-/.exec(name);
|
|
45
45
|
return m ? Number(m[1]) : null;
|
|
46
46
|
}
|
|
47
|
+
/** The queue-file name one stamp below `stamps`' minimum — naming an entry to sort before
|
|
48
|
+
* every listed one, the "move to head" rename both queues share. With no parseable stamps it
|
|
49
|
+
* falls back to just before now (before every hand-placed name). Decrements while the computed
|
|
50
|
+
* name exists on disk, because a concurrent writer may have just taken the name we computed;
|
|
51
|
+
* the pid in queueFileName keeps two concurrent tops' names apart too. `ext` is the queue's own
|
|
52
|
+
* entry extension. The caller hands in its already-listed stamps (parsed with queueFileStamp),
|
|
53
|
+
* not a fresh scan, so the smallest stamp reflects the same read that picked the entry being
|
|
54
|
+
* moved. */
|
|
55
|
+
export function lowestStampName(stamps, dir, ext) {
|
|
56
|
+
let stamp = (stamps.length > 0 ? Math.min(...stamps) : Date.now()) - 1;
|
|
57
|
+
let file = path.join(dir, queueFileName(stamp, 0, ext));
|
|
58
|
+
while (fs.existsSync(file)) {
|
|
59
|
+
stamp--;
|
|
60
|
+
file = path.join(dir, queueFileName(stamp, 0, ext));
|
|
61
|
+
}
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
47
64
|
/** Remove one queue entry's file, reporting whether it was there: true when removed, false
|
|
48
65
|
* when it had already vanished (ENOENT). Any other error is rethrown. Both queues treat a
|
|
49
66
|
* vanished file as a normal race with a concurrent dequeue/cancel, never as a failure. */
|
package/dist/src/files/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { errCode } from "../errno.js";
|
|
4
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
4
5
|
/** Generic file operations under the harness's error policy — missing is no data, cleanup
|
|
5
6
|
* must not throw, directories are created before writes: stat-or-missing for log readers,
|
|
6
7
|
* PATH lookup for the pi-installation preflight, size-based rotation, recursive directory
|
|
@@ -22,6 +23,22 @@ export function statOrNull(file) {
|
|
|
22
23
|
return null; // Missing (or vanished) — no data yet.
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
/** Whether `file` exists and is a directory (a symlink is followed); false when it is missing
|
|
27
|
+
* or cannot be stat'd. */
|
|
28
|
+
export function isDirectory(file) {
|
|
29
|
+
return statOrNull(file)?.isDirectory() === true;
|
|
30
|
+
}
|
|
31
|
+
/** Whether `file` is a regular file the process may execute (a symlink is followed); false
|
|
32
|
+
* when it is missing, a directory (which passes X_OK), or lacks the executable bit. */
|
|
33
|
+
export function isExecutableFile(file) {
|
|
34
|
+
try {
|
|
35
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
36
|
+
return fs.statSync(file).isFile();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
25
42
|
/** Whether a freshly stat'd file is still the same file at the same bytes as a prior
|
|
26
43
|
* observation: matching dev/ino (a replaced inode is a different file even at the same size)
|
|
27
44
|
* plus matching size and, when the caller tracks it, mtime. The one home of the freshness
|
|
@@ -36,6 +53,12 @@ export function sameStatIdentity(st, prev) {
|
|
|
36
53
|
return false;
|
|
37
54
|
return prev.mtimeMs === undefined || st.mtimeMs === prev.mtimeMs;
|
|
38
55
|
}
|
|
56
|
+
/** The identity fields sameStatIdentity compares, all zero when the file is missing (`null`):
|
|
57
|
+
* the "empty log" observation, so a log that has not been created yet reads as zero bytes of a
|
|
58
|
+
* file no real stat can match. */
|
|
59
|
+
export function statIdentityOrZero(st) {
|
|
60
|
+
return { dev: st?.dev ?? 0, ino: st?.ino ?? 0, size: st?.size ?? 0, mtimeMs: st?.mtimeMs ?? 0 };
|
|
61
|
+
}
|
|
39
62
|
/** Open `file` for reading, or null when it vanished between a prior stat and this open — the
|
|
40
63
|
* shared race-handling step for the incremental/tail log readers (forEachTailChunk,
|
|
41
64
|
* terminateTornTail, readCompleteLines, readTranscriptTail), all of which treat a rotated-away
|
|
@@ -83,7 +106,7 @@ export function terminateTornTail(file, st) {
|
|
|
83
106
|
/** Read a text file whole, or null when it does not exist (or cannot be read) — the harness's
|
|
84
107
|
* "missing is no data" policy for the markdown and prompt text a render or prompt-building
|
|
85
108
|
* path must never throw on: the backlog's PLANS/BUGS/QUESTIONS, the principles injected into
|
|
86
|
-
* every
|
|
109
|
+
* every run prompt, the usage report's sources, and a queued prompt that vanished mid-
|
|
87
110
|
* listing. The single home of that read-and-swallow step; callers pick their empty value
|
|
88
111
|
* (`?? ""` when a blank default reads better than null). */
|
|
89
112
|
export function readTextOrNull(file) {
|
|
@@ -148,29 +171,37 @@ export function envPath() {
|
|
|
148
171
|
/** Locate an executable on PATH the same way spawn() would resolve it: a regular file
|
|
149
172
|
* with the execute bit in some PATH directory. Returns its absolute path, or null when
|
|
150
173
|
* missing (or not executable), so callers can fail fast with a clear message instead of
|
|
151
|
-
* letting every
|
|
174
|
+
* letting every run die with "spawn <name> ENOENT". */
|
|
152
175
|
export function findOnPath(name, pathEnv = envPath()) {
|
|
153
176
|
for (const dir of pathEnv.split(path.delimiter)) {
|
|
154
177
|
if (!dir)
|
|
155
178
|
continue;
|
|
156
179
|
const candidate = path.join(dir, name);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (fs.statSync(candidate).isFile())
|
|
160
|
-
return candidate;
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// Not in this directory; keep looking.
|
|
164
|
-
}
|
|
180
|
+
if (isExecutableFile(candidate))
|
|
181
|
+
return candidate;
|
|
165
182
|
}
|
|
166
183
|
return null;
|
|
167
184
|
}
|
|
168
185
|
/** Keep an append-only log bounded: over `maxBytes` it is renamed to `<file>.1`
|
|
169
|
-
* (replacing any previous rotation) and a fresh file starts. Returns true if rotated.
|
|
170
|
-
|
|
186
|
+
* (replacing any previous rotation) and a fresh file starts. Returns true if rotated.
|
|
187
|
+
* `keep` is how many archived generations to retain: over the size it drops `<file>.<keep>`,
|
|
188
|
+
* shifts `<file>.<k-1>` → `<file>.<k>` from oldest to newest (missing generations skipped),
|
|
189
|
+
* then moves the live file to `<file>.1`. With `keep` 1 it behaves as before, replacing the
|
|
190
|
+
* previous `.1`; role pi logs pass the default, the harness event log keeps five generations
|
|
191
|
+
* (EVENTS_KEEP). */
|
|
192
|
+
export function rotateIfLarge(file, maxBytes, keep = 1) {
|
|
171
193
|
try {
|
|
172
194
|
if (fs.statSync(file).size <= maxBytes)
|
|
173
195
|
return false;
|
|
196
|
+
if (keep >= 1) {
|
|
197
|
+
// Drop the oldest generation first so a shift down cannot collide with it.
|
|
198
|
+
fs.rmSync(file + "." + keep, { force: true });
|
|
199
|
+
for (let gen = keep - 1; gen >= 1; gen--) {
|
|
200
|
+
const from = file + "." + gen;
|
|
201
|
+
if (fs.existsSync(from))
|
|
202
|
+
fs.renameSync(from, file + "." + (gen + 1));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
174
205
|
fs.renameSync(file, file + ".1");
|
|
175
206
|
return true;
|
|
176
207
|
}
|
|
@@ -217,13 +248,9 @@ export function walkUp(startDir, maxLevels, visit) {
|
|
|
217
248
|
* cannot drift between them. (files.ts's own pruneOldFiles keeps its hand-rolled walk: it
|
|
218
249
|
* removes emptied directories on the way back up, which this file-only visitor cannot do.) */
|
|
219
250
|
export function walkFiles(dir, visit, skipDirs = new Set()) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
251
|
+
const entries = tryReadDirEntries(dir);
|
|
252
|
+
if (!entries)
|
|
225
253
|
return; // Vanished or unreadable — skip this directory.
|
|
226
|
-
}
|
|
227
254
|
for (const entry of entries) {
|
|
228
255
|
const file = path.join(dir, entry.name);
|
|
229
256
|
if (entry.isDirectory()) {
|
|
@@ -241,22 +268,44 @@ export function walkFiles(dir, visit, skipDirs = new Set()) {
|
|
|
241
268
|
* with the same walk-and-swallow error policy, so it lives here once. (walkFiles above is the
|
|
242
269
|
* recursive, file-only sibling.) */
|
|
243
270
|
export function subdirectoryNames(dir, keep) {
|
|
244
|
-
|
|
271
|
+
const entries = tryReadDirEntries(dir);
|
|
272
|
+
if (!entries)
|
|
273
|
+
return []; // Missing or unreadable — no data.
|
|
274
|
+
const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
275
|
+
return keep ? names.filter(keep) : names;
|
|
276
|
+
}
|
|
277
|
+
/** The entry names directly under `dir`, in readdir order; `[]` when the directory is missing
|
|
278
|
+
* or unreadable. For the best-effort scans — a sweep, a count, an existence probe — where "no
|
|
279
|
+
* directory" and "nothing in it" are the same answer. A reader that must tell an unreadable
|
|
280
|
+
* directory from an empty one (listQueueFiles) keeps its own try/catch. */
|
|
281
|
+
export function dirEntryNames(dir) {
|
|
245
282
|
try {
|
|
246
|
-
|
|
283
|
+
return fs.readdirSync(dir);
|
|
247
284
|
}
|
|
248
285
|
catch {
|
|
249
|
-
return [];
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** The `Dirent` list directly under `dir`, or null when the directory is missing or
|
|
290
|
+
* unreadable — the walk-and-swallow prelude walkFiles, subdirectoryNames, pruneOldFiles, and
|
|
291
|
+
* treeBytes all began by spelling out, so the "a vanished or unreadable directory is skipped,
|
|
292
|
+
* never an error" policy lives in one place. `dirEntryNames` above is the plain-name sibling
|
|
293
|
+
* for scans that need no file/directory distinction. build-stage.ts's pruneStaleStagings reads
|
|
294
|
+
* the staging root the same way, and exports it the way dirEntryNames is exported.) */
|
|
295
|
+
export function tryReadDirEntries(dir) {
|
|
296
|
+
try {
|
|
297
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
250
301
|
}
|
|
251
|
-
const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
252
|
-
return keep ? names.filter(keep) : names;
|
|
253
302
|
}
|
|
254
303
|
/** Write `data` — UTF-8 text or raw bytes — to `file` via a tmp file + rename, so a concurrent
|
|
255
304
|
* reader of the same path sees either the old (absent) or the new content, never a partial
|
|
256
305
|
* write: the prompt-inbox queue files (inbox.ts's enqueueRolePrompt) are written by one process
|
|
257
|
-
* while dashboards poll, `--list` reads, and the
|
|
258
|
-
* the write could
|
|
259
|
-
* name carries the pid because several processes can write at once (CLI, TUI, GUI, a
|
|
306
|
+
* while dashboards poll, `--list` reads, and the roles dequeue in others, and a read that races
|
|
307
|
+
* the write could start an agent run on a truncated user request or flash one on the dashboard.
|
|
308
|
+
* The tmp name carries the pid because several processes can write at once (CLI, TUI, GUI, a role's
|
|
260
309
|
* re-queue); on failure the tmp is removed and the error rethrown, leaving the target untouched.
|
|
261
310
|
* The harness's JSON writer (writeJsonAtomic in src/files/json-files.ts) and the config write-back
|
|
262
311
|
* (writeBytesAtomic's caller, merge-git.ts's restoreConfigBytes) write through this, so the
|
|
@@ -304,6 +353,11 @@ export function removeQuiet(file) {
|
|
|
304
353
|
export function removeTree(dir) {
|
|
305
354
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
306
355
|
}
|
|
356
|
+
/** The epoch-ms instant `days` days before now: the age cutoff the retention prunes compare a
|
|
357
|
+
* file's mtime against (pruneOldFiles, pruneOldDirectory, and session-ledger.ts's two prunes). */
|
|
358
|
+
export function daysAgoMs(days) {
|
|
359
|
+
return Date.now() - days * DAY_MS;
|
|
360
|
+
}
|
|
307
361
|
/** Delete regular files under `dir` (recursively) older than `days` days. Walks with a plain
|
|
308
362
|
* per-directory readdir instead of {recursive: true} + entry.parentPath, so it stays within the
|
|
309
363
|
* Node >= 20 floor declared in package.json — parentPath landed only in v20.12, and on earlier
|
|
@@ -324,16 +378,12 @@ export function removeTree(dir) {
|
|
|
324
378
|
export function pruneOldFiles(dir, days) {
|
|
325
379
|
if (!fs.existsSync(dir))
|
|
326
380
|
return 0;
|
|
327
|
-
const cutoff =
|
|
381
|
+
const cutoff = daysAgoMs(days);
|
|
328
382
|
let pruned = 0;
|
|
329
383
|
const walk = (d) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
384
|
+
const entries = tryReadDirEntries(d);
|
|
385
|
+
if (!entries)
|
|
335
386
|
return; // Vanished (or unreadable) mid-scan; skip this directory.
|
|
336
|
-
}
|
|
337
387
|
for (const entry of entries) {
|
|
338
388
|
const file = path.join(d, entry.name);
|
|
339
389
|
if (entry.isDirectory()) {
|
|
@@ -369,14 +419,14 @@ export function pruneOldFiles(dir, days) {
|
|
|
369
419
|
}
|
|
370
420
|
/** Remove `dir` entirely when the directory itself is older than `days` days (its own mtime,
|
|
371
421
|
* not its children's). Worktree builds leave a gitignored `dist/` dir that `git clean -fd`
|
|
372
|
-
* does not drop; this prunes it at the next
|
|
422
|
+
* does not drop; this prunes it at the next run, bounded by the same age rule. Returns true
|
|
373
423
|
* when the directory was removed. Mirrors pruneOldFiles' vanish-tolerant style. */
|
|
374
424
|
export function pruneOldDirectory(dir, days) {
|
|
375
425
|
if (!fs.existsSync(dir))
|
|
376
426
|
return false;
|
|
377
427
|
let pruned = false;
|
|
378
428
|
try {
|
|
379
|
-
if (fs.statSync(dir).mtimeMs <
|
|
429
|
+
if (fs.statSync(dir).mtimeMs < daysAgoMs(days)) {
|
|
380
430
|
removeTree(dir);
|
|
381
431
|
pruned = true;
|
|
382
432
|
}
|
|
@@ -386,3 +436,29 @@ export function pruneOldDirectory(dir, days) {
|
|
|
386
436
|
}
|
|
387
437
|
return pruned;
|
|
388
438
|
}
|
|
439
|
+
/** The total bytes under one directory tree, counted iteratively and skipping unreadable
|
|
440
|
+
* entries: a permission-denied subdirectory must not fail doctor. */
|
|
441
|
+
export function treeBytes(dir) {
|
|
442
|
+
let total = 0;
|
|
443
|
+
const stack = [dir];
|
|
444
|
+
while (stack.length > 0) {
|
|
445
|
+
const current = stack.pop();
|
|
446
|
+
const entries = tryReadDirEntries(current);
|
|
447
|
+
if (!entries)
|
|
448
|
+
continue;
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
const full = path.join(current, entry.name);
|
|
451
|
+
if (entry.isDirectory())
|
|
452
|
+
stack.push(full);
|
|
453
|
+
else if (entry.isFile()) {
|
|
454
|
+
try {
|
|
455
|
+
total += fs.statSync(full).size;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// A file that vanished mid-walk contributes nothing.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return total;
|
|
464
|
+
}
|
|
@@ -8,14 +8,14 @@ import { parseJsonObject } from "./json-object.js";
|
|
|
8
8
|
* is "no data", never an error) shared by every reader and writer of those files. */
|
|
9
9
|
/** Read and parse a JSON *object* file, returning null when it does not exist, cannot be read
|
|
10
10
|
* or parsed, or parses to a valid JSON value that is not a plain object (a scalar, `null`, or
|
|
11
|
-
* an array). The harness's state/marker/info files (
|
|
11
|
+
* an array). The harness's state/marker/info files (runner state, orchestrator info, landing
|
|
12
12
|
* marker, reset/wake/abort markers, restart stamp, build stamp, claude account state) are all
|
|
13
13
|
* objects written by other processes and may be missing or torn mid-write — readers treat that
|
|
14
14
|
* as "no data" rather than an error, so a crash in one process can never take down the
|
|
15
15
|
* observers polling them. A wrong-shaped file reads the same way: casting a scalar/array to `T`
|
|
16
16
|
* would hand
|
|
17
|
-
* callers a value that looks like data but is not (
|
|
18
|
-
* characters into the
|
|
17
|
+
* callers a value that looks like data but is not (loadRunnerState would spread a string's
|
|
18
|
+
* characters into the role's state, readOrchestratorInfo's `pid` would read undefined off an
|
|
19
19
|
* array), so the object check is part of the no-data policy, not an extra one. */
|
|
20
20
|
export function readJsonFile(file) {
|
|
21
21
|
try {
|
|
@@ -30,11 +30,11 @@ export function stringList(value) {
|
|
|
30
30
|
* absent, not a number, or not finite (NaN/±Infinity poison arithmetic and comparisons: a
|
|
31
31
|
* NaN costUsd makes every budget-cap comparison false, so the cap never trips) — the one
|
|
32
32
|
* home of the "the field is the number or it is nothing" read. The call sites, by the
|
|
33
|
-
* fallback each passes: event-read.ts's
|
|
34
|
-
* 0), eventDayKey (ts → null),
|
|
33
|
+
* fallback each passes: event-read.ts's eventRun (run → 0), eventUsage (tokens, costUsd →
|
|
34
|
+
* 0), eventDayKey (ts → null), runStartMap and runSpanMs (both ts → null);
|
|
35
35
|
* event-format.ts's elapsed (ms → null), gigabytes (v → 0), minutes (ms → 0), and the
|
|
36
36
|
* build_stale, slot_wait, restart, and permit_reserve renderers (aheadCommits, waitedMs,
|
|
37
|
-
*
|
|
37
|
+
* abortedRuns, reserve → 0); phrases.ts's budgetPhrase (spentUsd, capUsd → 0); redeploy.ts's
|
|
38
38
|
* autoRestartRecord (lastAt → null); build/build-info.ts's readBuildInfo (builtAt → 0);
|
|
39
39
|
* failure/failure-state-change.ts's model_fallback_ended (durationMs → 0) and build_stale
|
|
40
40
|
* renderer (aheadCommits → 0); gates/maintenance-quota.ts's foldLine and seed (ev.ts → null);
|
|
@@ -54,13 +54,13 @@ export function finiteNumber(value, fallback) {
|
|
|
54
54
|
* amount is as poisonous as NaN: it runs a spend total backwards and makes every `>= cap`
|
|
55
55
|
* comparison false, so the daily budget's safety valve would never trip. The call sites, by
|
|
56
56
|
* the fallback each passes: budget/budget.ts's spendNumber (dayCostUsd/usd → 0),
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* runner/runner-state-load.ts's healNumber (every numeric RunnerState field → 0),
|
|
58
|
+
* run/run-usage.ts's counter (each persisted usage count → 0), pi/pi-stream.ts's usageNumber
|
|
59
59
|
* (one wire usage number → 0), ui/progress-data.ts's message_end read (totalTokens → the
|
|
60
|
-
* previous context, output and its peak floor → 0), time-spend.ts's
|
|
60
|
+
* previous context, output and its peak floor → 0), time-spend.ts's runDurationMs (an event's
|
|
61
61
|
* durationMs → null, the "no own duration" marker), pi-extension/context-budget.ts's
|
|
62
62
|
* contextNote (usedTokens, usedWindow → null), pi-extension/context-shake.ts's percentOf
|
|
63
|
-
* (tokens, contextWindow → null), merge/merge-queue.ts's isMergeEntry (
|
|
63
|
+
* (tokens, contextWindow → null), merge/merge-queue.ts's isMergeEntry (run, enqueuedAt,
|
|
64
64
|
* revisionRound → null), git/slots-state.ts's isSlotLease (since, pid → null); and
|
|
65
65
|
* agent/claude/claude-stream.ts's canonicalUsage (each wire usage
|
|
66
66
|
* number → 0) and its result/retry/rate-limit reads (api_error_status, attempt,
|
|
@@ -92,6 +92,27 @@ export function parseJsonObject(text) {
|
|
|
92
92
|
export function objectOrEmpty(value) {
|
|
93
93
|
return isJsonObject(value) ? value : {};
|
|
94
94
|
}
|
|
95
|
+
/** The entries of `value` that pass `guard`, as a fresh record; `{}` when `value` is not a JSON
|
|
96
|
+
* object. The one home of "keep the well-formed entries of an untrusted map, drop the foreign
|
|
97
|
+
* ones" (the image-probe and opencode-catalog caches, the per-pair daily spend map). Built with
|
|
98
|
+
* `Object.fromEntries`, so a `__proto__` key stays an own property. */
|
|
99
|
+
export function validEntries(value, guard) {
|
|
100
|
+
if (!isJsonObject(value))
|
|
101
|
+
return {};
|
|
102
|
+
return Object.fromEntries(Object.entries(value).filter((kv) => guard(kv[1])));
|
|
103
|
+
}
|
|
104
|
+
/** Store one own property on `obj` under `key`. A plain `obj[key] = value` loses a key of
|
|
105
|
+
* `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
|
|
106
|
+
* instead of adding an own entry, so the value vanishes from every reader and from the
|
|
107
|
+
* persisted JSON. Handling untrusted keys, or a model/flow name crossing an external boundary,
|
|
108
|
+
* means that case is real, so every site that stores under such a key defines the own property
|
|
109
|
+
* instead — the one home of the `enumerable/writable/configurable: true` descriptor, so the
|
|
110
|
+
* three flags cannot drift between sites. A named wrapper (`setWindow` on the account's
|
|
111
|
+
* windows) keeps its domain meaning across its call sites; callers keep `setOwn` where the
|
|
112
|
+
* key/value are generic, as the qa coverage ledger and budget.ts's per-pair spend do. */
|
|
113
|
+
export function setOwn(obj, key, value) {
|
|
114
|
+
Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
|
|
115
|
+
}
|
|
95
116
|
/** Assemble a stream translator's canonical usage block from the four already-validated non-
|
|
96
117
|
* negative counts and the USD cost — the one home of the shape both the claude and opencode
|
|
97
118
|
* translators (agent/claude/claude-stream.ts, agent/opencode/opencode-stream.ts) build, so the
|
|
@@ -36,8 +36,8 @@ export function cachedByStat(cache, key, file, load, clone) {
|
|
|
36
36
|
* reads it, and a null (no data) propagates through so a render path never throws on the file's
|
|
37
37
|
* state. `parse` runs only on a cache miss and receives the file's text (never null); `clone`
|
|
38
38
|
* hands each caller its own copy. The callers: backlog.ts's sectionEntries and
|
|
39
|
-
* sectionCompletionDates, and backlog-structure.ts's strandedPlanEntriesFor
|
|
40
|
-
* actionableEntryRangesFor, and renderBacklogIndexBlock. */
|
|
39
|
+
* sectionCompletionDates, and backlog-structure.ts's strandedPlanEntriesFor and
|
|
40
|
+
* actionableEntryRangesFor, and backlog-index.ts's renderBacklogIndexBlock. */
|
|
41
41
|
export function cachedTextByStat(cache, key, file, parse, clone) {
|
|
42
42
|
return cachedByStat(cache, key, file, () => {
|
|
43
43
|
const md = readTextOrNull(file); // Missing or unreadable — no data.
|
package/dist/src/files/tail.js
CHANGED
|
@@ -160,7 +160,7 @@ const MAX_TAILS = 64;
|
|
|
160
160
|
* from `fresh()`'s offset and folding every line into its fresh value;
|
|
161
161
|
* - append-only growth since the last visit → fold only the newly complete lines in.
|
|
162
162
|
* A torn trailing line is left unconsumed until its newline lands (the offset advances to
|
|
163
|
-
* the last complete line). Shared by every observer that polls a log once per
|
|
163
|
+
* the last complete line). Shared by every observer that polls a log once per run —
|
|
164
164
|
* src/ui/progress-data.ts's live tail and transcript.ts's rendered entries. */
|
|
165
165
|
export function withTail(tails, file, st, fresh, feed) {
|
|
166
166
|
let tail = tails.get(file);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** New-project bootstrap (plans/work-ratio.md, "New-project bootstrap"; part 2/2): a fresh
|
|
3
2
|
* project seeds `bootstrap.untilPlansDone` in tumwater.json (part 1/2), and this gate holds
|
|
4
|
-
* every maintenance
|
|
5
|
-
* feature and director
|
|
3
|
+
* every maintenance role until that many plans have moved to PLANS.md's `## Done`. Only plan,
|
|
4
|
+
* feature and director run — plus bugfix while BUGS.md `## Open` is non-empty, since its
|
|
6
5
|
* latent-bug search is maintenance. The hold gives a young project a steady stream of plans and
|
|
7
6
|
* code before hygiene roles start churning.
|
|
8
7
|
*
|
|
@@ -28,7 +27,7 @@ import { writeJsonAtomic } from "../files/json-files.js";
|
|
|
28
27
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
29
28
|
import { errorMessage, stripTerminalControls } from "../text/text.js";
|
|
30
29
|
import { BUGFIX_ROLE, DIRECTOR_ROLE } from "../roles/roles.js";
|
|
31
|
-
import { baseRoleOf } from "../roles/
|
|
30
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
32
31
|
/** The read-only bootstrap verdict: active while a positive `untilPlansDone` is configured and
|
|
33
32
|
* the latch file is absent and fewer than that many plans are Done. No writes — the status
|
|
34
33
|
* surfaces (src/status/status-data.ts) call this directly to show the same figure the
|
|
@@ -56,7 +55,7 @@ export function bootstrapHoldsRole(role, openBugsNow) {
|
|
|
56
55
|
export function newBootstrapGateState() {
|
|
57
56
|
return { active: false };
|
|
58
57
|
}
|
|
59
|
-
/** Step the gate by one orchestrator poll. Returns the
|
|
58
|
+
/** Step the gate by one orchestrator poll. Returns the role ids held by bootstrap as of THIS
|
|
60
59
|
* poll, for the scheduler's `bootstrapHeld` view. When the target has been reached, writes the
|
|
61
60
|
* latch and logs `bootstrap_complete` once (the now-present latch suppresses later logs, and
|
|
62
61
|
* survives an orchestrator restart that resets `state`). An unwritable repo does not abort the
|