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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** The pre-queue self-check's acceptance-grep runner: a finished plan entry that says a `grep …`
|
|
2
|
+
* "finds nothing" is checked by running that grep in the worktree, so a change is not rejected
|
|
3
|
+
* for an acceptance grep that still matches. Only the strict "matches nothing" form runs;
|
|
4
|
+
* criteria worded "prints only …" need judgement and stay out. Staging-only (stage-check.ts). */
|
|
5
|
+
import { execFileAsync } from "../process/process.js";
|
|
6
|
+
import { moreSuffix } from "../text/phrases.js";
|
|
7
|
+
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
8
|
+
const MAX_CRITERIA = 5;
|
|
9
|
+
const GREP_TIMEOUT_MS = 20_000;
|
|
10
|
+
const EMPTY_PHRASE_RE = new RegExp("^\\s*(?:(?:finds|prints|returns|outputs|matches|lists) nothing|is empty|lists none|" +
|
|
11
|
+
"(?:prints|finds|lists|returns|gives|shows) no (?:lines?|hits?|matches?|output))", "i");
|
|
12
|
+
const ALLOWANCE_RE = /apart from|except|excluding|other than|besides|beyond/i;
|
|
13
|
+
/** The backticked `grep …`/`git grep …` commands in `body` that the text says match nothing
|
|
14
|
+
* ("finds nothing", "is empty", …), without duplicates, in body order, at most five. A criterion
|
|
15
|
+
* whose sentence (up to the next `.` or `;`) allows some hits ("apart from", "except", …) is
|
|
16
|
+
* dropped. */
|
|
17
|
+
export function emptyGrepCriteria(body) {
|
|
18
|
+
const text = collapseWhitespace(body);
|
|
19
|
+
const found = [];
|
|
20
|
+
for (const m of text.matchAll(/`([^`]+)`/g)) {
|
|
21
|
+
const cmd = m[1] ?? "";
|
|
22
|
+
if (!/^(?:git )?grep /.test(cmd))
|
|
23
|
+
continue;
|
|
24
|
+
const rest = text.slice((m.index ?? 0) + m[0].length);
|
|
25
|
+
const phrase = EMPTY_PHRASE_RE.exec(rest);
|
|
26
|
+
if (phrase === null)
|
|
27
|
+
continue;
|
|
28
|
+
const sentence = rest.slice(0, rest.search(/[.;]/) === -1 ? undefined : rest.search(/[.;]/));
|
|
29
|
+
if (ALLOWANCE_RE.test(sentence))
|
|
30
|
+
continue;
|
|
31
|
+
if (!found.includes(cmd))
|
|
32
|
+
found.push(cmd);
|
|
33
|
+
if (found.length >= MAX_CRITERIA)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/** A minimal shell-word split of a `grep …`/`git grep …` command, or undefined when it uses
|
|
39
|
+
* shell syntax (an unquoted `|;&<>$()` or backtick) or leaves a quote open. Nothing runs through
|
|
40
|
+
* a shell, so `*` stays literal. */
|
|
41
|
+
export function grepArgv(cmd) {
|
|
42
|
+
const words = [];
|
|
43
|
+
let cur = "";
|
|
44
|
+
let inWord = false;
|
|
45
|
+
let quote = null;
|
|
46
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
47
|
+
const c = cmd[i];
|
|
48
|
+
if (quote === "'") {
|
|
49
|
+
if (c === "'")
|
|
50
|
+
quote = null;
|
|
51
|
+
else
|
|
52
|
+
cur += c;
|
|
53
|
+
}
|
|
54
|
+
else if (quote === '"') {
|
|
55
|
+
const next = cmd[i + 1];
|
|
56
|
+
if (c === "\\" && (next === '"' || next === "\\")) {
|
|
57
|
+
cur += next;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
else if (c === '"')
|
|
61
|
+
quote = null;
|
|
62
|
+
else
|
|
63
|
+
cur += c;
|
|
64
|
+
}
|
|
65
|
+
else if (c === "'" || c === '"') {
|
|
66
|
+
quote = c;
|
|
67
|
+
inWord = true;
|
|
68
|
+
}
|
|
69
|
+
else if (c === "\\") {
|
|
70
|
+
if (i + 1 < cmd.length)
|
|
71
|
+
cur += cmd[++i];
|
|
72
|
+
inWord = true;
|
|
73
|
+
}
|
|
74
|
+
else if (/[|;&<>$()`]/.test(c)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
else if (/\s/.test(c)) {
|
|
78
|
+
if (inWord)
|
|
79
|
+
words.push(cur);
|
|
80
|
+
cur = "";
|
|
81
|
+
inWord = false;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
cur += c;
|
|
85
|
+
inWord = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (quote !== null)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (inWord)
|
|
91
|
+
words.push(cur);
|
|
92
|
+
if (words[0] === "git" && words[1] === "grep")
|
|
93
|
+
return { file: "git", args: words.slice(1) };
|
|
94
|
+
if (words[0] === "grep")
|
|
95
|
+
return { file: "grep", args: words.slice(1) };
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/** Run each entry's matches-nothing greps in `wt`; one finding per entry, for its first grep that
|
|
99
|
+
* still prints lines. An exit of 1 passes; any other failure (exit 2, timeout, spawn error) skips
|
|
100
|
+
* the criterion, since an unrunnable command is not evidence. */
|
|
101
|
+
export async function acceptanceGrepFindings(wt, entries) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
for (const cmd of emptyGrepCriteria(entry.body)) {
|
|
105
|
+
const argv = grepArgv(cmd);
|
|
106
|
+
if (argv === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
const out = await execFileAsync(argv.file, argv.args, { cwd: wt, timeout: GREP_TIMEOUT_MS }).then((r) => r.stdout, () => "");
|
|
109
|
+
const lines = out.split("\n").filter((l) => l !== "");
|
|
110
|
+
if (lines.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
const shown = lines.slice(0, 5).map((l) => truncate(l, 200)).join("; ");
|
|
113
|
+
findings.push(`PLANS.md's finished entry "${truncate(entry.title, 160)}" says \`${cmd}\` matches nothing, but it prints ` +
|
|
114
|
+
`${lines.length} line(s): ${shown}${moreSuffix(lines.length - 5)} — fix those lines, or correct the claim if the criterion is wrong`);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** The `ci` observer's CI-parity ledger (plans, part 3/3). Every `ci` run starts a fresh pi
|
|
2
|
+
* session, so the only durable memory of which main tip and `package-lock.json` each run
|
|
3
|
+
* covered is a runtime file the harness — not pi — maintains. The role runs the workflow's own
|
|
4
|
+
* full suite and e2e, which is minutes of work, so its nothing-to-do decision must be cheap:
|
|
5
|
+
* the parity block tells the fresh session what the last run covered, and when main has not
|
|
6
|
+
* moved (and the lock is unchanged) since a green run, the role declares nothing-to-do in one
|
|
7
|
+
* model run instead of re-running the whole suite. The ledger is gitignored runtime state
|
|
8
|
+
* under .tumwater/state/ and degrades to "no data" on any missing or malformed file: an
|
|
9
|
+
* observer must never fail a run on bookkeeping (the same policy as qa-coverage.ts). A stale
|
|
10
|
+
* verdict that stays stale past `CI_PARITY_STALE_MS` is a stall — the full suite has silently
|
|
11
|
+
* stopped covering new tips — surfaced as a doctor warn and a standing `ci-stale` alert through
|
|
12
|
+
* the one `ciParityStalled` predicate. */
|
|
13
|
+
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
14
|
+
import { ciParityPath } from "../paths.js";
|
|
15
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
16
|
+
import { currentBranchFromHeadFile, readBranchHead, targetBranch } from "../git/git.js";
|
|
17
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
18
|
+
import * as crypto from "node:crypto";
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
/** A stale verdict that stays stale past this window is a stall: the `ci` role — now the
|
|
22
|
+
* full suite's own runner — has silently stopped covering new tips (disabled, paused, wedged,
|
|
23
|
+
* or failing before it records). The doctor `ci parity` warn and the standing `ci-stale` alert
|
|
24
|
+
* share this one threshold, so the surfaces cannot disagree on when stale is a problem. */
|
|
25
|
+
export const CI_PARITY_STALE_MS = 3 * DAY_MS;
|
|
26
|
+
/** True only when a parity record exists, reads `stale` (a `failed` record already warns and
|
|
27
|
+
* a `current` one is verified for the tip), and has not covered a tip within the stall
|
|
28
|
+
* window. Parameter is structural — not `CiParityStatus` — so the status-polls → ci-parity
|
|
29
|
+
* import edge stays one-way. This is the single stall predicate shared by the doctor row and
|
|
30
|
+
* the alert. */
|
|
31
|
+
export function ciParityStalled(parity, now) {
|
|
32
|
+
return !!parity && parity.state === "stale" && now - parity.lastRunAt > CI_PARITY_STALE_MS;
|
|
33
|
+
}
|
|
34
|
+
/** True when `value` is a well-formed parity record. A record missing any field, or carrying
|
|
35
|
+
* a non-`passed|failed` result, reads as no data — the caller renders no block and the run
|
|
36
|
+
* proceeds as before. */
|
|
37
|
+
function isParityEntry(value) {
|
|
38
|
+
if (!isJsonObject(value))
|
|
39
|
+
return false;
|
|
40
|
+
return (typeof value.tip === "string" &&
|
|
41
|
+
typeof value.lockHash === "string" &&
|
|
42
|
+
(value.result === "passed" || value.result === "failed") &&
|
|
43
|
+
typeof value.lastRunAt === "number");
|
|
44
|
+
}
|
|
45
|
+
/** Read the parity ledger. A missing, torn, or wrong-shaped file reads as null — the caller
|
|
46
|
+
* renders no block and the run proceeds as before. */
|
|
47
|
+
export function readCiParity(root) {
|
|
48
|
+
const file = readJsonFile(ciParityPath(root));
|
|
49
|
+
return file?.parity && isParityEntry(file.parity) ? file.parity : null;
|
|
50
|
+
}
|
|
51
|
+
/** Record what a `ci` run covered: the tip it named on its `CI:` line and the verdict that
|
|
52
|
+
* line carried, plus the lock hash of the coverage tree computed here. The single record is
|
|
53
|
+
* written atomically (writeJsonAtomic) so a concurrent reader never sees a torn file; a
|
|
54
|
+
* concurrent last-writer simply wins the slot. */
|
|
55
|
+
export function recordParity(root, sha, result, now = Date.now()) {
|
|
56
|
+
writeJsonAtomic(ciParityPath(root), { parity: { tip: sha, lockHash: lockHashOf(root), result, lastRunAt: now } });
|
|
57
|
+
}
|
|
58
|
+
/** The hash of `package-lock.json` at `root` itself — what the current tree carries. An
|
|
59
|
+
* empty string when the file is missing (or unreadable) reads as an unmatched hash, so a
|
|
60
|
+
* swing to "no lock file" renders stale rather than pretending nothing moved. */
|
|
61
|
+
export function lockHashOf(root) {
|
|
62
|
+
let file;
|
|
63
|
+
try {
|
|
64
|
+
file = fs.readFileSync(path.join(root, "package-lock.json"), "utf8");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
return crypto.createHash("sha256").update(file).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
/** The current main tip and lock hash, resolved synchronously from the ref files and the
|
|
72
|
+
* working tree (mirroring status-polls.ts's per-poll read: readBranchHead + targetBranch, no
|
|
73
|
+
* spawn). Either degrades to "" when it cannot be resolved, which the render treats as
|
|
74
|
+
* "has definitely not matched", so a resolution failure can never declare a false current. */
|
|
75
|
+
export function currentCiParityState(root, config) {
|
|
76
|
+
const branch = targetBranch(config?.baseBranch, currentBranchFromHeadFile(root));
|
|
77
|
+
return { tip: readBranchHead(root, branch) ?? "", lockHash: lockHashOf(root) };
|
|
78
|
+
}
|
|
79
|
+
/** True when `entry` counts as "current": the last run passed AND it covered exactly this
|
|
80
|
+
* tip with exactly this lock hash. This is the single home of the "current" decision, so the
|
|
81
|
+
* ci prompt block (`renderParityBlock`) and the doctor `ci parity` row (`checkCiParity`) cannot
|
|
82
|
+
* disagree on what "current" means. `entry.tip` is whatever the run named on its `CI:` line — a
|
|
83
|
+
* 7-64-char abbreviated sha (the model's own shorthand) or the full 40-char head; match it as a
|
|
84
|
+
* prefix of the current tip so an abbreviated recorded tip still reads current once the full
|
|
85
|
+
* head shares that prefix. */
|
|
86
|
+
export function parityIsCurrent(entry, tip, lockHash) {
|
|
87
|
+
return entry.result === "passed" && tip !== "" && tip.startsWith(entry.tip) && entry.lockHash === lockHash;
|
|
88
|
+
}
|
|
89
|
+
/** Shorten an id for display without losing its discriminating prefix (a 40-char sha and a
|
|
90
|
+
* 64-char lock hash both collapse to a 7-char prefix the model can compare). */
|
|
91
|
+
function shortId(id) {
|
|
92
|
+
return id.length > 7 ? id.slice(0, 7) : id;
|
|
93
|
+
}
|
|
94
|
+
/** Render the parity block for the next `ci` prompt from the last record and the current
|
|
95
|
+
* main state. "current" only when the last run was green AND it covered exactly this tip with
|
|
96
|
+
* exactly this lock hash — the cheap nothing-to-do case. Every other combination — a failed
|
|
97
|
+
* last run, main moved, or the lock changed — renders "stale" naming both the covered and the
|
|
98
|
+
* current tip/hash, so the role runs the suite. */
|
|
99
|
+
export function renderParityBlock(entry, tip, lockHash) {
|
|
100
|
+
const lines = ["CI parity (from this team's own record):"];
|
|
101
|
+
if (parityIsCurrent(entry, tip, lockHash)) {
|
|
102
|
+
lines.push(` current — main is unmoved since the last green run covered tip ${shortId(entry.tip)} with lock ${shortId(entry.lockHash)}; declare nothing-to-do without running the suite.`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const covered = `${shortId(entry.tip)} @ ${shortId(entry.lockHash)} (${entry.result})`;
|
|
106
|
+
const now = tip === "" ? "could not be resolved" : `tip ${shortId(tip)} @ ${shortId(lockHash)}`;
|
|
107
|
+
lines.push(` stale — the last run covered ${covered}; main is now ${now}. Run the full suite and e2e before declaring anything.`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
/** The full parity block for a `ci` prompt, or undefined when there is no record yet — a
|
|
112
|
+
* missing or malformed ledger must render nothing rather than an empty verdict, so the very
|
|
113
|
+
* first run carries no block and simply runs the suite (the same omission policy as qa's
|
|
114
|
+
* coverage block). */
|
|
115
|
+
export function parityBlockFor(root, config) {
|
|
116
|
+
const record = readCiParity(root);
|
|
117
|
+
if (!record)
|
|
118
|
+
return undefined;
|
|
119
|
+
const { tip, lockHash } = currentCiParityState(root, config);
|
|
120
|
+
return renderParityBlock(record, tip, lockHash);
|
|
121
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
2
|
+
import { feedbackFilingsPath } from "../paths.js";
|
|
3
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
4
|
+
import { formatAge } from "../text/datetime.js";
|
|
5
|
+
/** The `feedback` observer's per-install filing ledger (plans/feedback-role.md 2/2). Every
|
|
6
|
+
* `feedback` run starts a fresh pi session, so the only durable record of which GitHub issues
|
|
7
|
+
* this install already filed must live in harness state — the same cross-run memory qa's
|
|
8
|
+
* flow-coverage ledger provides (plans/observer-roles.md 2/2), and what makes the next run's
|
|
9
|
+
* prompt able to render a "previously filed" block so the charter's dedupe does not rely on the
|
|
10
|
+
* network alone. The ledger is gitignored runtime state under .tumwater/state/ and degrades to
|
|
11
|
+
* "no data" on any missing or malformed file: an observer must never fail a run on bookkeeping
|
|
12
|
+
* (a filing that fails to persist is a warning, never a run failure). */
|
|
13
|
+
/** The newest bound the ledger keeps. A fresh session files at most one issue per run, so this
|
|
14
|
+
* caps the block's growth and the prompt's size; older filings are dropped (the charter still
|
|
15
|
+
* dedupes against the live `gh issue list`). */
|
|
16
|
+
export const FEEDBACK_FILINGS_MAX = 100;
|
|
17
|
+
function isFiling(value) {
|
|
18
|
+
if (!isJsonObject(value))
|
|
19
|
+
return false;
|
|
20
|
+
return (typeof value.issue === "number" &&
|
|
21
|
+
typeof value.title === "string" &&
|
|
22
|
+
typeof value.filedAt === "number");
|
|
23
|
+
}
|
|
24
|
+
/** Read the ledger, keeping only well-formed entries. A missing, torn, or wrong-shaped file
|
|
25
|
+
* reads as an empty ledger — the caller renders no block and the run proceeds as before. */
|
|
26
|
+
export function readFeedbackFilings(root) {
|
|
27
|
+
const file = readJsonFile(feedbackFilingsPath(root));
|
|
28
|
+
const filings = file?.filings;
|
|
29
|
+
if (!Array.isArray(filings))
|
|
30
|
+
return [];
|
|
31
|
+
return filings.filter(isFiling);
|
|
32
|
+
}
|
|
33
|
+
/** Record that this install filed issue `issue` now (or at `now`, for tests). Reads-modifies-
|
|
34
|
+
* writes the whole ledger atomically so a concurrent writer cannot leave a torn file behind,
|
|
35
|
+
* and keeps only the newest FEEDBACK_FILINGS_MAX entries. */
|
|
36
|
+
export function recordFiled(root, issue, title, now = Date.now()) {
|
|
37
|
+
const filings = readFeedbackFilings(root);
|
|
38
|
+
const entry = { issue, title, filedAt: now };
|
|
39
|
+
writeJsonAtomic(feedbackFilingsPath(root), {
|
|
40
|
+
filings: [entry, ...filings].slice(0, FEEDBACK_FILINGS_MAX),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Render the "previously filed" block for the next `feedback` prompt; undefined when the
|
|
44
|
+
* ledger is empty (the common first-run case), so the prompt is unchanged then. Newest-first,
|
|
45
|
+
* each line `#<n> <title> (<age>)` mirroring qa's coverage lines. */
|
|
46
|
+
export function renderFilingsBlock(filings, now = Date.now()) {
|
|
47
|
+
if (filings.length === 0)
|
|
48
|
+
return undefined;
|
|
49
|
+
const lines = filings.map((f) => ` #${f.issue} ${f.title} (${formatAge(now - f.filedAt)})`);
|
|
50
|
+
return ["Previously filed from this install:", ...lines].join("\n");
|
|
51
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
2
|
+
import { qaCoveragePath } from "../paths.js";
|
|
3
|
+
import { isJsonObject, setOwn } from "../files/json-object.js";
|
|
4
|
+
import { DAY_MS, formatAge } from "../text/datetime.js";
|
|
5
|
+
/** The `qa` observer's flow-coverage ledger (plans/observer-roles.md 2/2). Every `qa` run
|
|
6
|
+
* starts a fresh pi session, so the only durable memory of which flow it last exercised is a
|
|
7
|
+
* runtime file the harness — not pi — maintains. A passing cheap check writes nothing into the
|
|
8
|
+
* repo (a note commit per check would move main and wake every sleeping role), so without this
|
|
9
|
+
* ledger the model picks a flow blind every run and converges on the top of its menu. The
|
|
10
|
+
* ledger is gitignored runtime state under .tumwater/state/ and degrades to "no data" on any
|
|
11
|
+
* missing or malformed file: an observer must never fail a run on bookkeeping. */
|
|
12
|
+
/** The flows `qa` exercises, cheapest-first — the ordered menu from plans/qa-role.md, plus the
|
|
13
|
+
* expensive `run (real)` variant. The names are the universe the coverage block renders; a flow
|
|
14
|
+
* recorded by name but absent here (a project-specific flow) still renders, appended after. */
|
|
15
|
+
export const QA_FLOWS = [
|
|
16
|
+
"init",
|
|
17
|
+
"status",
|
|
18
|
+
"logs",
|
|
19
|
+
"prompt",
|
|
20
|
+
"reset-counters",
|
|
21
|
+
"gui",
|
|
22
|
+
"tui",
|
|
23
|
+
"run",
|
|
24
|
+
"run (real)",
|
|
25
|
+
];
|
|
26
|
+
/** A coverage ledger whose newest exercise is older than this window is a stall: the `qa`
|
|
27
|
+
* role — the team's only flow-level observer, the one role that exercises the product's own
|
|
28
|
+
* flows like a user — has silently stopped recording any exercise (disabled, paused, wedged,
|
|
29
|
+
* or failing before it records). The doctor `qa coverage` warn and the standing `qa-stale`
|
|
30
|
+
* alert share this one threshold, mirroring `ciParityStalled`'s `CI_PARITY_STALE_MS`. */
|
|
31
|
+
export const QA_COVERAGE_STALE_MS = 3 * DAY_MS;
|
|
32
|
+
/** True only when the ledger has at least one well-formed entry and every entry's `lastRunAt`
|
|
33
|
+
* is older than the stall window (`now - max(lastRunAt) > QA_COVERAGE_STALE_MS`). An empty
|
|
34
|
+
* ledger — a missing file, or a project that never ran qa — is not a stall: "no record yet" is
|
|
35
|
+
* not "stopped recording", the same rule `ciParityStalled` applies to a missing parity record.
|
|
36
|
+
* This is the one stall predicate the doctor row (`checkQaCoverage`) and the alert
|
|
37
|
+
* (`qaCoverageAlert`) share, so the surfaces cannot disagree on when a quiet qa is a problem. */
|
|
38
|
+
export function qaCoverageStalled(coverage, now) {
|
|
39
|
+
let newest = 0;
|
|
40
|
+
let has = false;
|
|
41
|
+
for (const entry of Object.values(coverage)) {
|
|
42
|
+
has = true;
|
|
43
|
+
if (entry.lastRunAt > newest)
|
|
44
|
+
newest = entry.lastRunAt;
|
|
45
|
+
}
|
|
46
|
+
return has && now - newest > QA_COVERAGE_STALE_MS;
|
|
47
|
+
}
|
|
48
|
+
function isResult(value) {
|
|
49
|
+
return value === "passed" || value === "bug";
|
|
50
|
+
}
|
|
51
|
+
/** Read the ledger, keeping only well-formed entries. A missing, torn, or wrong-shaped file
|
|
52
|
+
* reads as an empty ledger — the caller renders no block and the run proceeds as before. */
|
|
53
|
+
export function readQaCoverage(root) {
|
|
54
|
+
const file = readJsonFile(qaCoveragePath(root));
|
|
55
|
+
const flows = file?.flows;
|
|
56
|
+
if (!isJsonObject(flows))
|
|
57
|
+
return {};
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [name, raw] of Object.entries(flows)) {
|
|
60
|
+
if (!isJsonObject(raw))
|
|
61
|
+
continue;
|
|
62
|
+
const entry = raw;
|
|
63
|
+
if (typeof entry.lastRunAt !== "number" || !isResult(entry.result))
|
|
64
|
+
continue;
|
|
65
|
+
setOwn(out, name, {
|
|
66
|
+
lastRunAt: entry.lastRunAt,
|
|
67
|
+
result: entry.result,
|
|
68
|
+
...(typeof entry.summary === "string" ? { summary: entry.summary } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** Record that `flow` was exercised now (or at `now`, for tests). Reads-modifies-writes the
|
|
74
|
+
* whole ledger atomically so a concurrent writer cannot leave a torn file behind. */
|
|
75
|
+
export function recordFlow(root, flow, result, summary, now = Date.now()) {
|
|
76
|
+
const coverage = readQaCoverage(root);
|
|
77
|
+
setOwn(coverage, flow, { lastRunAt: now, result, ...(summary ? { summary } : {}) });
|
|
78
|
+
writeJsonAtomic(qaCoveragePath(root), { flows: coverage });
|
|
79
|
+
}
|
|
80
|
+
/** The coverage as ordered rows — the one universe (menu plus recorded project-specific flows)
|
|
81
|
+
* and one ordering (never-exercised first, then exercised stalest-first, ties in menu order)
|
|
82
|
+
* that the next `qa` prompt's block and the operator-facing views share, so they cannot drift.
|
|
83
|
+
* Never-exercised flows lead (age = ∞), then exercised flows oldest-first, so "exercise the
|
|
84
|
+
* flow at the top" actually rotates through the whole menu instead of re-picking whatever is
|
|
85
|
+
* cheapest. The flow universe is `QA_FLOWS` plus any recorded name (a project-specific flow
|
|
86
|
+
* still surfaces, appended after the menu). */
|
|
87
|
+
export function coverageRows(coverage, now = Date.now()) {
|
|
88
|
+
const names = [...QA_FLOWS];
|
|
89
|
+
for (const name of Object.keys(coverage)) {
|
|
90
|
+
if (!names.includes(name))
|
|
91
|
+
names.push(name);
|
|
92
|
+
}
|
|
93
|
+
return names
|
|
94
|
+
.map((name, index) => ({ name, index, entry: coverage[name] }))
|
|
95
|
+
.sort((a, b) => {
|
|
96
|
+
const ageA = a.entry ? now - a.entry.lastRunAt : Number.POSITIVE_INFINITY;
|
|
97
|
+
const ageB = b.entry ? now - b.entry.lastRunAt : Number.POSITIVE_INFINITY;
|
|
98
|
+
if (ageA !== ageB)
|
|
99
|
+
return ageB - ageA; // stalest first; never-exercised (∞) leads
|
|
100
|
+
return a.index - b.index; // stable: keep the menu order among equals
|
|
101
|
+
})
|
|
102
|
+
.map(({ name, entry }) => ({
|
|
103
|
+
flow: name,
|
|
104
|
+
lastRunAt: entry?.lastRunAt ?? null,
|
|
105
|
+
result: entry?.result ?? null,
|
|
106
|
+
...(entry?.summary !== undefined ? { summary: entry.summary } : {}),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/** Render the coverage block for the next `qa` prompt — the same rows `coverageRows` orders,
|
|
110
|
+
* one line each (a two-space-prefixed row, never-exercised flows first). */
|
|
111
|
+
export function renderCoverageBlock(coverage, now = Date.now()) {
|
|
112
|
+
const lines = coverageRows(coverage, now).map(({ flow, lastRunAt, result, summary }) => {
|
|
113
|
+
if (lastRunAt === null)
|
|
114
|
+
return ` ${flow} — never exercised`;
|
|
115
|
+
const age = formatAge(now - lastRunAt);
|
|
116
|
+
const status = result === "bug"
|
|
117
|
+
? `bug filed (BUGS.md: "${summary ?? "see BUGS.md"}")`
|
|
118
|
+
: "passed";
|
|
119
|
+
return ` ${flow} — ${age} ago, ${status}`;
|
|
120
|
+
});
|
|
121
|
+
return [
|
|
122
|
+
"Flow coverage (from this team's own record; least recently exercised first):",
|
|
123
|
+
...lines,
|
|
124
|
+
].join("\n");
|
|
125
|
+
}
|