tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,24 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
/** Backlog eligibility: which PLANS.md / BUGS.md entries a work loop may take now (plans,
|
|
1
|
+
/** Backlog eligibility: which PLANS.md / BUGS.md entries a work role may take now (plans,
|
|
3
2
|
* parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused …**,
|
|
4
3
|
* **Needs review …**, **Needs replan …** or **Waiting for the next failure …** note, or when
|
|
5
4
|
* its heading's trailing parenthetical
|
|
6
5
|
* (or a parenthetical left open by a wrapped heading) names a prerequisite (`requires parts
|
|
7
6
|
* 1/5–3/5 landed`) that is itself still listed under `## Planned`. The parsers are pure over
|
|
8
7
|
* markdown text; `eligibleEntries` is the stat-backed reader for a caller that wants the list.
|
|
9
|
-
* The backlog index marks every held entry so a
|
|
8
|
+
* The backlog index marks every held entry so a role
|
|
10
9
|
* skips it without re-deriving the rule. */
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import { statOrNull } from "../files/files.js";
|
|
13
|
-
import { allLiveClaims } from "../
|
|
14
|
-
import {
|
|
15
|
-
import { NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX, WAITING_FOR_FAILURE_PREFIX } from "../roles/role-guidance.js";
|
|
16
|
-
import { baseRoleOf } from "../roles/
|
|
12
|
+
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
|
+
import { firstTrimmedLineStartingWith } from "../text/text.js";
|
|
14
|
+
import { HELD_TAG_PREFIX, NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX, PRIOR_HELD_PREFIX, WAITING_FOR_FAILURE_PREFIX } from "../roles/role-guidance.js";
|
|
15
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
17
16
|
import { roleLabel } from "../roles/role-labels.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { actionableEntryRangesFor } from "./backlog-structure.js";
|
|
18
|
+
import { entryKey, partKey, plannedPartKeys, plansWaitingOn, requiredParts, seriesPart } from "./backlog-parts.js";
|
|
20
19
|
import { finishedFiles, openBugEntries, plannedPlanEntries } from "./backlog.js";
|
|
21
|
-
/** The
|
|
20
|
+
/** The number of holds an entry's count must reach before a Needs-review/Needs-replan note
|
|
21
|
+
* becomes a `held-for-operator` hold (the operator's decision, surfaced by parts 2/3 and 3/3). */
|
|
22
|
+
export const OPERATOR_HOLD_THRESHOLD = 3;
|
|
23
|
+
/** The held count a body records: the integer in the first Needs-review or Needs-replan note
|
|
24
|
+
* line's `(held N)` tag, else the integer in a `**Previously held N**` line, else 0. A present
|
|
25
|
+
* but non-numeric `(held …)` tag is 0 (and stops the scan), so a malformed tag never throws.
|
|
26
|
+
* Pure over the body text — the data-layer read half of the convergence backstop. */
|
|
27
|
+
export function heldCount(body) {
|
|
28
|
+
const tagRe = new RegExp(`${HELD_TAG_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)\\)`);
|
|
29
|
+
for (const raw of body.split("\n")) {
|
|
30
|
+
const line = raw.trimStart();
|
|
31
|
+
const note = line.startsWith(NEEDS_REVIEW_PREFIX) || line.startsWith(NEEDS_REPLAN_PREFIX);
|
|
32
|
+
if (!note)
|
|
33
|
+
continue;
|
|
34
|
+
const tag = tagRe.exec(line);
|
|
35
|
+
if (tag !== null)
|
|
36
|
+
return Number(tag[1]);
|
|
37
|
+
if (line.includes(HELD_TAG_PREFIX))
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
for (const raw of body.split("\n")) {
|
|
41
|
+
const line = raw.trimStart();
|
|
42
|
+
if (!line.startsWith(PRIOR_HELD_PREFIX))
|
|
43
|
+
continue;
|
|
44
|
+
const count = /\d+/.exec(line.slice(PRIOR_HELD_PREFIX.length));
|
|
45
|
+
return count === null ? 0 : Number(count[0]);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/** The `(held N)` count on a single Needs-review or Needs-replan note line, or undefined when the
|
|
50
|
+
* line is not such a note or carries no numeric tag. The per-line form of heldCount, so a writer
|
|
51
|
+
* that drops notes (withoutHeldForOperatorNotes) can gate on the same threshold without scanning
|
|
52
|
+
* the whole body. */
|
|
53
|
+
function lineHeldCount(line) {
|
|
54
|
+
const trimmed = line.trimStart();
|
|
55
|
+
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX) || trimmed.startsWith(NEEDS_REPLAN_PREFIX);
|
|
56
|
+
if (!isNote)
|
|
57
|
+
return undefined;
|
|
58
|
+
const tag = new RegExp(`${HELD_TAG_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)\\)`).exec(trimmed);
|
|
59
|
+
return tag === null ? undefined : Number(tag[1]);
|
|
60
|
+
}
|
|
61
|
+
/** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
|
|
22
62
|
* imported Needs-review and Needs-replan prefixes — is matched as a line prefix (hasNoteLine)
|
|
23
63
|
* so a mention in prose never holds an entry. */
|
|
24
64
|
const REFUSED_PREFIX = "**Refused ";
|
|
@@ -32,7 +72,7 @@ function hasNoteLine(body, prefix) {
|
|
|
32
72
|
* begins with it, trimmed, with the leading `**` and the first closing `**` removed. Null when
|
|
33
73
|
* no line matches — the same line-prefix rule entryHold uses. */
|
|
34
74
|
function noteText(body, prefix) {
|
|
35
|
-
const line = body
|
|
75
|
+
const line = firstTrimmedLineStartingWith(body, prefix);
|
|
36
76
|
return line === undefined ? null : line.replace(/^\*\*/, "").replace("**", "");
|
|
37
77
|
}
|
|
38
78
|
/** The text of `body`'s Refused note, or null when it has none (entryHold's "refused" hold). */
|
|
@@ -69,53 +109,51 @@ export function withoutRefusalNotes(lines) {
|
|
|
69
109
|
export function withoutWaitingNotes(lines) {
|
|
70
110
|
return withoutNoteLines(lines, WAITING_FOR_FAILURE_PREFIX);
|
|
71
111
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function partKey(series, part) {
|
|
118
|
-
return `${series.toLowerCase()}\u0000${part}`;
|
|
112
|
+
/** `lines` with every Needs-review/Needs-replan note whose `(held N)` count reached
|
|
113
|
+
* OPERATOR_HOLD_THRESHOLD removed, together with its `**Previously held N−1**` line (the prior
|
|
114
|
+
* count the increment rule recorded beside the live `(held N)`), so a `--release` lift leaves
|
|
115
|
+
* neither behind and the entry becomes eligible. Drops nothing when no note is at or past the
|
|
116
|
+
* threshold, so release still cannot lift a live first/second Needs-review hold. Consecutive
|
|
117
|
+
* dropped lines are collapsed to one blank line, the same rule withoutNoteLines applies to a
|
|
118
|
+
* single dropped note. */
|
|
119
|
+
export function withoutHeldForOperatorNotes(lines) {
|
|
120
|
+
const drop = new Set();
|
|
121
|
+
const priorHeld = new Set();
|
|
122
|
+
lines.forEach((raw, i) => {
|
|
123
|
+
const c = lineHeldCount(raw);
|
|
124
|
+
if (c !== undefined && c >= OPERATOR_HOLD_THRESHOLD) {
|
|
125
|
+
priorHeld.add(c - 1);
|
|
126
|
+
drop.add(i);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (priorHeld.size === 0)
|
|
130
|
+
return lines;
|
|
131
|
+
lines.forEach((raw, i) => {
|
|
132
|
+
if (drop.has(i))
|
|
133
|
+
return;
|
|
134
|
+
const trimmed = raw.trimStart();
|
|
135
|
+
if (!trimmed.startsWith(PRIOR_HELD_PREFIX))
|
|
136
|
+
return;
|
|
137
|
+
const count = /\d+/.exec(trimmed.slice(PRIOR_HELD_PREFIX.length));
|
|
138
|
+
if (count !== null && priorHeld.has(Number(count[0])))
|
|
139
|
+
drop.add(i);
|
|
140
|
+
});
|
|
141
|
+
const out = [];
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
if (!drop.has(i)) {
|
|
144
|
+
out.push(lines[i]);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let j = i;
|
|
148
|
+
while (j < lines.length && drop.has(j))
|
|
149
|
+
j++;
|
|
150
|
+
// A dropped run between two blank lines drops the blank after it too, so the lift leaves
|
|
151
|
+
// one blank line where the note was (the same rule withoutNoteLines applies to one).
|
|
152
|
+
if (j < lines.length && (lines[i - 1] ?? "x").trim() === "" && (lines[j] ?? "x").trim() === "")
|
|
153
|
+
j++;
|
|
154
|
+
i = j - 1;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
119
157
|
}
|
|
120
158
|
/** The Done entries of the parts `title` requires, in the order its requires clause names them:
|
|
121
159
|
* for each requiredParts ref, the first `## Done` entry whose seriesPart has the same partKey,
|
|
@@ -164,178 +202,9 @@ export function waitingPartRanges(root, title) {
|
|
|
164
202
|
}
|
|
165
203
|
return out;
|
|
166
204
|
}
|
|
167
|
-
/** The prerequisite-set identities of the still-planned entries — seriesPart(title) of each,
|
|
168
|
-
* keyed by partKey — built once for a batch and reused across every entry judged against it, so
|
|
169
|
-
* judging n entries is n seriesPart parses rather than n² (entryHold rebuilt the set per entry).
|
|
170
|
-
* Series compare case-insensitively and part tokens verbatim, exactly as entryHoldAgainst
|
|
171
|
-
* matches them. */
|
|
172
|
-
export function plannedPartKeys(planned) {
|
|
173
|
-
const keys = new Set();
|
|
174
|
-
for (const p of planned) {
|
|
175
|
-
const part = seriesPart(p.title);
|
|
176
|
-
if (part !== null)
|
|
177
|
-
keys.add(partKey(part.series, part.part));
|
|
178
|
-
}
|
|
179
|
-
return keys;
|
|
180
|
-
}
|
|
181
|
-
/** The doctor's count of a refusal's stranded dependents: the ascending indexes into `planned` of
|
|
182
|
-
* the entries that require `planned[index]`'s own part, directly or through a chain of parts.
|
|
183
|
-
* Matching is by partKey, as entryHoldAgainst matches. [] when that entry has no series part. A
|
|
184
|
-
* dependency cycle ends the walk, and `index` itself is never listed. */
|
|
185
|
-
export function plansWaitingOn(planned, index) {
|
|
186
|
-
const root = planned[index];
|
|
187
|
-
const own = root === undefined ? null : seriesPart(root.title);
|
|
188
|
-
if (own === null)
|
|
189
|
-
return [];
|
|
190
|
-
const dependents = new Map();
|
|
191
|
-
planned.forEach((entry, i) => {
|
|
192
|
-
if (seriesPart(entry.title) === null)
|
|
193
|
-
return;
|
|
194
|
-
for (const ref of requiredParts(entry.title)) {
|
|
195
|
-
const key = partKey(ref.series, ref.part);
|
|
196
|
-
dependents.set(key, [...(dependents.get(key) ?? []), i]);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
const seen = new Set([index]);
|
|
200
|
-
const queue = [partKey(own.series, own.part)];
|
|
201
|
-
for (let key = queue.shift(); key !== undefined; key = queue.shift()) {
|
|
202
|
-
for (const i of dependents.get(key) ?? []) {
|
|
203
|
-
if (seen.has(i))
|
|
204
|
-
continue;
|
|
205
|
-
seen.add(i);
|
|
206
|
-
const part = seriesPart(planned[i].title);
|
|
207
|
-
queue.push(partKey(part.series, part.part));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
seen.delete(index);
|
|
211
|
-
return [...seen].sort((a, b) => a - b);
|
|
212
|
-
}
|
|
213
|
-
/** The prerequisite parts a plan heading's trailing parenthetical (or, when the heading wraps
|
|
214
|
-
* and the parenthetical stays open past the line, its open parenthetical) names through the clause
|
|
215
|
-
* `requires <ref>((, | and )<ref>)* landed`, with each range (e.g. `parts 1/5–3/5`) expanded.
|
|
216
|
-
* Returns [] when the heading has no such clause or the clause does not parse — an agent judges
|
|
217
|
-
* an unparseable clause, exactly as it does today. Only the heading is read, never the body. */
|
|
218
|
-
export function requiredParts(title) {
|
|
219
|
-
const clause = requiresClause(title);
|
|
220
|
-
if (clause === null)
|
|
221
|
-
return [];
|
|
222
|
-
const ownSeries = seriesPart(title)?.series ?? null;
|
|
223
|
-
const refs = [];
|
|
224
|
-
for (const chunk of clause.split(/\s*,\s*|\s+and\s+/i))
|
|
225
|
-
refs.push(...parseRef(chunk, ownSeries));
|
|
226
|
-
return refs;
|
|
227
|
-
}
|
|
228
|
-
/** The ref text a heading's `requires <refs> landed` clause captures — read from the trailing
|
|
229
|
-
* parenthetical, or the open one of a wrapped heading — or null when the heading has no such
|
|
230
|
-
* clause. requiredParts parses it; planDependencyProblems uses it to spot a clause that parses to
|
|
231
|
-
* nothing. */
|
|
232
|
-
function requiresClause(title) {
|
|
233
|
-
let meta = trailingParenthetical(title);
|
|
234
|
-
if (meta === "")
|
|
235
|
-
meta = openParenthetical(title);
|
|
236
|
-
if (meta === "")
|
|
237
|
-
return null;
|
|
238
|
-
const m = /requires\s+(.+?)\s+landed\b/i.exec(meta); // vocabulary-ok: parses the requires … landed heading syntax
|
|
239
|
-
return m === null ? null : m[1];
|
|
240
|
-
}
|
|
241
|
-
/** A dependency verb followed by a series part ref in a heading's parenthetical — the wording the
|
|
242
|
-
* claim allocator ignores (BUGS.md 2026-10-14: a part headed "blocked by part 1/2 above" was handed
|
|
243
|
-
* out six times while part 1/2 was in flight). planDependencyProblems reports each such ref. */
|
|
244
|
-
const MISWORDED_DEPENDENCY_RE = /\b(?:blocked by|blocked on|after|follows|depends on|waits on|waits for|once)\s+(?:the\s+)?(parts?\s+[0-9]+[a-z]?\/[0-9]+)/gi;
|
|
245
|
-
/** The mistakes in the planned entries' `requires … landed` clauses that the claim allocator
|
|
246
|
-
* cannot see: `dangling` — a ref naming a part no planned or done heading carries (its hold is
|
|
247
|
-
* void), `unparsed` — titles whose clause yields no refs, and `cycle` — the first dependency cycle
|
|
248
|
-
* among planned `Series, part i/n:` entries as `Series i/n` names in walk order, first name
|
|
249
|
-
* repeated last (a self-reference is a one-hop cycle), or null — and `misworded`: a series part
|
|
250
|
-
* whose heading parenthetical names a still-planned part through a dependency verb ("blocked by
|
|
251
|
-
* part 1/2") with no requires clause holding that part, so the allocator hands it out now.
|
|
252
|
-
* Refused and needs-review entries count like the rest, since their clauses still decide when
|
|
253
|
-
* they unblock. */
|
|
254
|
-
export function planDependencyProblems(planned, doneTitles) {
|
|
255
|
-
const known = plannedPartKeys(planned);
|
|
256
|
-
for (const t of doneTitles) {
|
|
257
|
-
const part = seriesPart(t);
|
|
258
|
-
if (part !== null)
|
|
259
|
-
known.add(partKey(part.series, part.part));
|
|
260
|
-
}
|
|
261
|
-
const dangling = [];
|
|
262
|
-
const unparsed = [];
|
|
263
|
-
const misworded = [];
|
|
264
|
-
const plannedKeys = plannedPartKeys(planned);
|
|
265
|
-
const nodes = new Map();
|
|
266
|
-
for (const entry of planned) {
|
|
267
|
-
const refs = requiredParts(entry.title);
|
|
268
|
-
if (refs.length === 0 && requiresClause(entry.title) !== null)
|
|
269
|
-
unparsed.push(entry.title);
|
|
270
|
-
for (const ref of refs) {
|
|
271
|
-
if (!known.has(partKey(ref.series, ref.part)))
|
|
272
|
-
dangling.push({ title: entry.title, ref: `${ref.series} ${ref.part}/${ref.of}` });
|
|
273
|
-
}
|
|
274
|
-
const own = seriesPart(entry.title);
|
|
275
|
-
if (own === null)
|
|
276
|
-
continue;
|
|
277
|
-
let meta = trailingParenthetical(entry.title);
|
|
278
|
-
if (meta === "")
|
|
279
|
-
meta = openParenthetical(entry.title);
|
|
280
|
-
const held = new Set(refs.map((ref) => partKey(ref.series, ref.part)));
|
|
281
|
-
for (const m of meta.matchAll(MISWORDED_DEPENDENCY_RE)) {
|
|
282
|
-
for (const ref of parseRef(m[1], own.series)) {
|
|
283
|
-
const key = partKey(ref.series, ref.part);
|
|
284
|
-
if (!plannedKeys.has(key) || held.has(key))
|
|
285
|
-
continue;
|
|
286
|
-
const name = `${ref.series} ${ref.part}/${ref.of}`;
|
|
287
|
-
if (!misworded.some((w) => w.title === entry.title && w.ref === name))
|
|
288
|
-
misworded.push({ title: entry.title, ref: name });
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
nodes.set(partKey(own.series, own.part), {
|
|
292
|
-
name: `${own.series} ${own.part}/${own.of}`,
|
|
293
|
-
deps: refs.map((ref) => partKey(ref.series, ref.part)),
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const state = new Map();
|
|
297
|
-
const stack = [];
|
|
298
|
-
const visit = (key) => {
|
|
299
|
-
state.set(key, "open");
|
|
300
|
-
stack.push(key);
|
|
301
|
-
for (const dep of nodes.get(key).deps) {
|
|
302
|
-
if (!nodes.has(dep))
|
|
303
|
-
continue;
|
|
304
|
-
const seen = state.get(dep);
|
|
305
|
-
if (seen === "open") {
|
|
306
|
-
return [...stack.slice(stack.indexOf(dep)), dep].map((k) => nodes.get(k).name);
|
|
307
|
-
}
|
|
308
|
-
if (seen === undefined) {
|
|
309
|
-
const found = visit(dep);
|
|
310
|
-
if (found !== null)
|
|
311
|
-
return found;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
stack.pop();
|
|
315
|
-
state.set(key, "done");
|
|
316
|
-
return null;
|
|
317
|
-
};
|
|
318
|
-
let cycle = null;
|
|
319
|
-
for (const key of nodes.keys()) {
|
|
320
|
-
if (state.has(key))
|
|
321
|
-
continue;
|
|
322
|
-
cycle = visit(key);
|
|
323
|
-
if (cycle !== null)
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
return { cycle, dangling, unparsed, misworded };
|
|
327
|
-
}
|
|
328
|
-
/** The prerequisite refs of `title`'s `requires … landed` clause when none of them is still in
|
|
329
|
-
* `plannedParts` (built by plannedPartKeys), else []. A heading with no parseable clause is never
|
|
330
|
-
* reported, so only a refusal whose named prerequisites have all left `## Planned` reads as
|
|
331
|
-
* possibly stale. */
|
|
332
|
-
export function prerequisitesMerged(title, plannedParts) {
|
|
333
|
-
const refs = requiredParts(title);
|
|
334
|
-
return refs.some((ref) => plannedParts.has(partKey(ref.series, ref.part))) ? [] : refs;
|
|
335
|
-
}
|
|
336
205
|
/** Why `entry` is held against the still-`planned` entries, or null when it may be taken: a
|
|
337
206
|
* `**Refused …` line (refused), a Needs-review line (needs-review), a Needs-replan line
|
|
338
|
-
* (needs-replan — the plan
|
|
207
|
+
* (needs-replan — the plan role owns it), or a prerequisite `(series, part)` still among
|
|
339
208
|
* `planned` ({ blockedBy }). Series compare case-insensitively; part tokens compare verbatim.
|
|
340
209
|
* Bodies mentioning "requires" are never consulted — only the heading's trailing or
|
|
341
210
|
* still-open parenthetical. */
|
|
@@ -348,10 +217,15 @@ export function entryHold(entry, planned) {
|
|
|
348
217
|
export function entryHoldAgainst(entry, plannedParts) {
|
|
349
218
|
if (hasNoteLine(entry.body, REFUSED_PREFIX))
|
|
350
219
|
return "refused";
|
|
351
|
-
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
220
|
+
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX) || hasNoteLine(entry.body, NEEDS_REPLAN_PREFIX)) {
|
|
221
|
+
// A note whose held count reached OPERATOR_HOLD_THRESHOLD stops out of the role loop:
|
|
222
|
+
// neither the plan role (planBacklogFull tests only these two) nor this role takes it.
|
|
223
|
+
return heldCount(entry.body) >= OPERATOR_HOLD_THRESHOLD
|
|
224
|
+
? "held-for-operator" // vocabulary-ok: the operator-held string names the operator hold decision
|
|
225
|
+
: hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX)
|
|
226
|
+
? "needs-review"
|
|
227
|
+
: "needs-replan";
|
|
228
|
+
}
|
|
355
229
|
if (hasNoteLine(entry.body, WAITING_FOR_FAILURE_PREFIX))
|
|
356
230
|
return "waiting-for-failure";
|
|
357
231
|
const refs = requiredParts(entry.title);
|
|
@@ -375,12 +249,22 @@ export function holdMark(hold) {
|
|
|
375
249
|
return " [blocked: needs review]";
|
|
376
250
|
if (hold === "needs-replan")
|
|
377
251
|
return " [blocked: needs replan]";
|
|
252
|
+
if (hold === "held-for-operator")
|
|
253
|
+
return " [blocked: held for operator]"; // vocabulary-ok: this hold names the operator-held wording for the operator decision
|
|
378
254
|
if (hold === "waiting-for-failure")
|
|
379
255
|
return " [blocked: waiting for the next failure]";
|
|
380
256
|
return ` [blocked: requires ${hold.blockedBy.join(", ")}]`;
|
|
381
257
|
}
|
|
258
|
+
/** The line `plan|bug --release` and `--top` print when the entry they touched is still held:
|
|
259
|
+
* empty for no hold, else the hold in holdMark wording and that no role takes the entry until it
|
|
260
|
+
* clears. */
|
|
261
|
+
export function stillHeldNote(hold) {
|
|
262
|
+
if (hold === null)
|
|
263
|
+
return "";
|
|
264
|
+
return `still held ${holdMark(hold).trim()}: no role takes it until that clears`;
|
|
265
|
+
}
|
|
382
266
|
/** `/api/status` polls the holds on the 1-second dashboard poll and the TUI recomputes them each
|
|
383
|
-
* frame while PLANS.md/BUGS.md change only when a
|
|
267
|
+
* frame while PLANS.md/BUGS.md change only when a role merges an edit. The file reads beneath are
|
|
384
268
|
* already stat-cached (plannedPlanEntries/openBugEntries), but the per-entry eligibility walk
|
|
385
269
|
* (entryHoldAgainst's body scan + heading clause) is pure CPU re-derived on every call, growing
|
|
386
270
|
* with the append-only backlog. Cache the derived holds keyed on both files' stat identities — the
|
|
@@ -447,16 +331,57 @@ export function backlogMarks(root) {
|
|
|
447
331
|
bugs: holds.bugs.map((h, i) => mark(h, working.bugs[i])),
|
|
448
332
|
};
|
|
449
333
|
}
|
|
450
|
-
/** The
|
|
451
|
-
*
|
|
452
|
-
* input)
|
|
334
|
+
/** The planned plans whose hold is `"held-for-operator"`, in file order: the plans parked
|
|
335
|
+
* until the operator lifts them with `tumwater plan --release` (the `operator-held` team
|
|
336
|
+
* alert's input), each carrying the backlog file and its 0-based position so a surface
|
|
337
|
+
* opening the row can land on it, the same RefusedEntry shape refusedEntries returns. Only
|
|
338
|
+
* plans — Needs-review/Needs-replan notes are a plan note, so a bug is never held this way.
|
|
339
|
+
* A missing file yields `[]`. */
|
|
340
|
+
export function operatorHeldEntries(root) {
|
|
341
|
+
const holds = backlogHolds(root);
|
|
342
|
+
const out = [];
|
|
343
|
+
plannedPlanEntries(root).forEach((e, i) => {
|
|
344
|
+
if (holds.plans[i] === "held-for-operator")
|
|
345
|
+
out.push({ file: "plans", index: i, title: e.title }); // vocabulary-ok: the held-for-operator hold's key
|
|
346
|
+
});
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/** The planned plans whose hold is exactly `"needs-review"` or `"needs-replan"`, in file
|
|
350
|
+
* order: the plans parked by a Needs-review/Needs-replan note until the plan role splits or
|
|
351
|
+
* replans them (the `stalled-plans` team alert's input), each carrying the 0-based PLANS.md
|
|
352
|
+
* index and the hold. Same `backlogHolds(root)` + `plannedPlanEntries(root)` pair
|
|
353
|
+
* `operatorHeldEntries` uses, so the alert and the doctor's `stalled plans` row classify
|
|
354
|
+
* through the same `entryHold` and cannot disagree on which holds are role-resolvable. A
|
|
355
|
+
* held-for-operator plan (a note at the held threshold) is excluded — it feeds
|
|
356
|
+
* `operatorHeldEntries` and the `operator-held` alert instead. A missing file yields `[]`. */
|
|
357
|
+
export function stalledPlanEntries(root) {
|
|
358
|
+
const holds = backlogHolds(root);
|
|
359
|
+
const out = [];
|
|
360
|
+
plannedPlanEntries(root).forEach((e, i) => {
|
|
361
|
+
const hold = holds.plans[i];
|
|
362
|
+
if (hold === "needs-review" || hold === "needs-replan")
|
|
363
|
+
out.push({ index: i, title: e.title, hold });
|
|
364
|
+
});
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
/** The planned plans, then the open bugs, whose hold is `"refused"`, in file order: the
|
|
368
|
+
* entries that wait on an operator decision (the `refused` team alert's input), each carrying
|
|
369
|
+
* the backlog file and its 0-based position so a surface opening the row can land on it.
|
|
370
|
+
* Pairs backlogHolds' arrays with the entries the same readers return, like
|
|
453
371
|
* backlogInProgress. A missing file yields `[]`. */
|
|
454
|
-
export function
|
|
372
|
+
export function refusedEntries(root) {
|
|
455
373
|
const holds = backlogHolds(root);
|
|
456
|
-
const
|
|
374
|
+
const entriesOf = (entries, held, file) => {
|
|
375
|
+
const out = [];
|
|
376
|
+
entries.forEach((e, i) => {
|
|
377
|
+
if (held[i] === "refused")
|
|
378
|
+
out.push({ file, index: i, title: e.title });
|
|
379
|
+
});
|
|
380
|
+
return out;
|
|
381
|
+
};
|
|
457
382
|
return [
|
|
458
|
-
...
|
|
459
|
-
...
|
|
383
|
+
...entriesOf(plannedPlanEntries(root), holds.plans, "plans"),
|
|
384
|
+
...entriesOf(openBugEntries(root), holds.bugs, "bugs"),
|
|
460
385
|
];
|
|
461
386
|
}
|
|
462
387
|
/** The runner id holding a live claim on each planned plan and open bug, two arrays parallel
|
|
@@ -475,7 +400,15 @@ export function backlogInProgress(root) {
|
|
|
475
400
|
export function inProgressMark(runner) {
|
|
476
401
|
return runner === null ? "" : ` [working on it: ${roleLabel(runner)}]`;
|
|
477
402
|
}
|
|
478
|
-
/** The
|
|
403
|
+
/** The note a cancel surface adds when the cancelled entry was claimed: empty for `null`, else
|
|
404
|
+
* names the role (through roleLabel) and the `abort` that stops its run, which the cancel itself
|
|
405
|
+
* leaves going. */
|
|
406
|
+
export function cancelledClaimNote(runner) {
|
|
407
|
+
return runner === null
|
|
408
|
+
? ""
|
|
409
|
+
: `${roleLabel(runner)} is still working on it — its run goes on; \`tumwater abort --role ${runner}\` stops it`;
|
|
410
|
+
}
|
|
411
|
+
/** The entries `role` may take now, in file order: PLANS.md's `## Planned` entries for
|
|
479
412
|
* feature, BUGS.md's `## Open` entries for bugfix, minus every entry entryHold holds, each with
|
|
480
413
|
* its stamp-free key, verbatim title and 1-based line range. Reads the primary checkout through
|
|
481
414
|
* the same stat-cached readers the index uses; a missing or unreadable file yields []. */
|
|
@@ -491,7 +424,7 @@ export function eligibleEntries(root, role) {
|
|
|
491
424
|
const parts = plannedPartKeys(planned);
|
|
492
425
|
// The cross-role in-flight hold (BUGS.md 2026-10-09): an entry another role's base has
|
|
493
426
|
// claimed is not eligible here. Same-base claims are excluded from the hold — the scheduling
|
|
494
|
-
// pass's claim groups and
|
|
427
|
+
// pass's claim groups and run-stage's staged claims already hold those — so the queried
|
|
495
428
|
// role's own claim (and a sibling instance's) stays eligible and its release rules are
|
|
496
429
|
// unchanged; only a plan another base currently owns is taken off this base's hands.
|
|
497
430
|
const ownBase = baseRoleOf(role);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Renders the `<backlog-index>` prompt block every role prompt carries — each actionable
|
|
2
|
+
* entry (PLANS.md ## Planned, BUGS.md ## Open, QUESTIONS.md ## Open) plus the newest closed
|
|
3
|
+
* ones, each with the 1-based line range it occupies in its file — the bounded replacement for
|
|
4
|
+
* the per-run `grep -n '^##'` heading map. The entry-range parsing (actionableEntryRanges) and
|
|
5
|
+
* series collapsing (groupBlockedSeries, partsLabel) it builds on live in backlog-structure.ts
|
|
6
|
+
* alongside the stranded-plan checks; this module holds only the block's rendering and its
|
|
7
|
+
* per-file stat caches. Extracted from backlog-structure.ts (organized 2026-10-19) so that
|
|
8
|
+
* module no longer carries a second responsibility. */
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
11
|
+
import { FINISHED_ARCHIVES } from "./backlog-md.js";
|
|
12
|
+
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
|
+
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
14
|
+
import { actionableEntryRanges, groupBlockedSeries, partsLabel, stripEntryStamp, } from "./backlog-structure.js";
|
|
15
|
+
import { entryHoldAgainst, holdMark } from "./backlog-eligibility.js";
|
|
16
|
+
import { plannedPartKeys } from "./backlog-parts.js";
|
|
17
|
+
import { plannedPlanEntries } from "./backlog.js";
|
|
18
|
+
/** Max characters an index title keeps before a trailing ellipsis, so one very long BUGS.md
|
|
19
|
+
* heading cannot dominate the block. */
|
|
20
|
+
const INDEX_TITLE_MAX = 160;
|
|
21
|
+
/** The index title for a `### ` heading: its stamp suffix (`(reported … 2026-…)`) removed via
|
|
22
|
+
* the shared stripEntryStamp and whitespace collapsed, then clipped to INDEX_TITLE_MAX
|
|
23
|
+
* through text.ts's truncate — the one ellipsis rule, so the cut cannot drift from the
|
|
24
|
+
* other trimmed labels and never splits a surrogate pair. */
|
|
25
|
+
function indexTitle(heading) {
|
|
26
|
+
return truncate(collapseWhitespace(stripEntryStamp(heading)), INDEX_TITLE_MAX);
|
|
27
|
+
}
|
|
28
|
+
/** The files and sections the actionable index covers, in the order roles read them: PLANS.md's
|
|
29
|
+
* planned features, BUGS.md's open bugs, QUESTIONS.md's open questions. */
|
|
30
|
+
const INDEX_SECTIONS = [
|
|
31
|
+
{ file: "PLANS.md", section: "Planned" },
|
|
32
|
+
{ file: "BUGS.md", section: "Open" },
|
|
33
|
+
{ file: "QUESTIONS.md", section: "Open" },
|
|
34
|
+
];
|
|
35
|
+
/** How many closed entries per closed section the index lists. */
|
|
36
|
+
const RECENT_CLOSED_MAX = 5;
|
|
37
|
+
/** The closed sections whose newest entries the index also lists, each read from the live file
|
|
38
|
+
* and then its archive (FINISHED_ARCHIVES, plans/backlog-archive.md); every one keeps its entries
|
|
39
|
+
* newest first, so the first entries in file order are the recent ones. */
|
|
40
|
+
const CLOSED_SECTIONS = FINISHED_ARCHIVES.map((a) => ({
|
|
41
|
+
files: [a.file, a.archive],
|
|
42
|
+
section: a.section,
|
|
43
|
+
}));
|
|
44
|
+
/** Per-file cache of the parsed actionable entries (indexSectionCache). */
|
|
45
|
+
const indexSectionCache = new Map();
|
|
46
|
+
/** Per-file cache of the newest closed entries' ranges; separate from indexSectionCache, which is
|
|
47
|
+
* keyed by the file path alone and PLANS.md would collide. */
|
|
48
|
+
const closedSectionCache = new Map();
|
|
49
|
+
/** One `IndexRow` as an ordinary index line: range, truncated stamp-free title, hold mark, and
|
|
50
|
+
* the in-flight suffix when a live claim names it. The collapsed-series line is built by
|
|
51
|
+
* collapseBlockedSeries, returned alongside these. */
|
|
52
|
+
function renderIndexRow(row) {
|
|
53
|
+
return `- ${row.start}-${row.end}: ${indexTitle(row.title)}${row.mark}${row.claimer ? ` [in flight by ${row.claimer}]` : ""}`;
|
|
54
|
+
}
|
|
55
|
+
function collapseBlockedSeries(rows) {
|
|
56
|
+
return groupBlockedSeries(rows).map((g) => {
|
|
57
|
+
if (g.kind === "row")
|
|
58
|
+
return renderIndexRow(g.row);
|
|
59
|
+
const first = g.rows[0];
|
|
60
|
+
const last = g.rows[g.rows.length - 1];
|
|
61
|
+
return (`- ${first.start}-${last.end}: ${indexTitle(g.series)}, ${g.rows.length} blocked parts ` +
|
|
62
|
+
`(${partsLabel(g.rows)}) [blocked: each waits on an earlier part]`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** The `<backlog-index>` prompt block: each actionable entry (PLANS.md ## Planned, BUGS.md
|
|
66
|
+
* ## Open, QUESTIONS.md ## Open) with its 1-based line range, rendered from the primary
|
|
67
|
+
* checkout `root`. Replaces the per-run `grep -n '^##'` heading map, whose pattern also matched
|
|
68
|
+
* every `###` title in BUGS.md's closed history and so re-sent ~108 KB on every turn (BUGS.md
|
|
69
|
+
* 2026-10-06). A missing or unreadable file contributes no sections; undefined when no section
|
|
70
|
+
* has entries, so an empty backlog leaves the prompt unchanged. The per-file actionable entries
|
|
71
|
+
* are stat-cached (indexSectionCache), so an unchanged backlog file is not re-read or re-walked. */
|
|
72
|
+
export function renderBacklogIndexBlock(root) {
|
|
73
|
+
// The still-planned set every section's holds are judged against. A prerequisite clause rides
|
|
74
|
+
// in the entry heading, and the eligibility gate (backlog-eligibility.ts's eligibleEntries)
|
|
75
|
+
// and `tumwater backlog`'s holds read it for BUGS.md's Open entries too, so the same set must
|
|
76
|
+
// reach this block's Open section as well — otherwise a held bug reads as eligible here while
|
|
77
|
+
// the gate skips it. plannedPlanEntries is stat-cached by sectionEntries, so an unchanged
|
|
78
|
+
// PLANS.md costs one stat per call.
|
|
79
|
+
const planned = plannedPlanEntries(root);
|
|
80
|
+
const plannedParts = plannedPartKeys(planned);
|
|
81
|
+
// The cross-role in-flight hold (BUGS.md 2026-10-09): every role's prompts share this one
|
|
82
|
+
// section-agnostic index, and the role it is assembled for is not available here, so an entry
|
|
83
|
+
// claimed by ANY role is marked in flight — the owner's own index marks it too, which is harmless
|
|
84
|
+
// (its assignment note is the instruction), and every other role reads it as taken instead of
|
|
85
|
+
// re-implementing a change someone's merge is already carrying to main.
|
|
86
|
+
const claimed = allLiveClaims(root);
|
|
87
|
+
const listed = [];
|
|
88
|
+
for (const { file, section } of INDEX_SECTIONS) {
|
|
89
|
+
const full = path.join(root, file);
|
|
90
|
+
const entries = cachedTextByStat(indexSectionCache, full, full, (md) => {
|
|
91
|
+
const ranges = actionableEntryRanges(md, section);
|
|
92
|
+
if (ranges.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const lines = md.split(/\r?\n/);
|
|
95
|
+
// The body slice is the lines after the heading through the last body line, so a note
|
|
96
|
+
// is found whether it sits first or last (the heading is at 1-based line e.start).
|
|
97
|
+
return ranges.map((e) => ({
|
|
98
|
+
start: e.start,
|
|
99
|
+
end: e.end,
|
|
100
|
+
title: e.title,
|
|
101
|
+
body: lines.slice(e.start, e.end).join("\n"),
|
|
102
|
+
}));
|
|
103
|
+
}, (v) => v.slice());
|
|
104
|
+
if (entries === null || entries.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
listed.push(`${file} ## ${section}`);
|
|
107
|
+
// Build the rows once per section, then collapse a PLANS.md series' blocked parts (this
|
|
108
|
+
// plan) — BUGS.md and QUESTIONS.md sections keep every entry on its own line.
|
|
109
|
+
const rows = entries.map((e) => {
|
|
110
|
+
const hold = entryHoldAgainst({ title: e.title, body: e.body }, plannedParts);
|
|
111
|
+
const key = collapseWhitespace(stripEntryStamp(e.title)).toLowerCase();
|
|
112
|
+
return {
|
|
113
|
+
start: e.start,
|
|
114
|
+
end: e.end,
|
|
115
|
+
title: e.title,
|
|
116
|
+
hold,
|
|
117
|
+
mark: holdMark(hold),
|
|
118
|
+
claimer: claimed.get(key),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const isPlans = file === "PLANS.md" && section === "Planned";
|
|
122
|
+
listed.push(...(isPlans ? collapseBlockedSeries(rows) : rows.map(renderIndexRow)));
|
|
123
|
+
}
|
|
124
|
+
for (const { files, section } of CLOSED_SECTIONS) {
|
|
125
|
+
// The newest RECENT_CLOSED_MAX across the live section (normally empty) and the archive.
|
|
126
|
+
let room = RECENT_CLOSED_MAX;
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (room <= 0)
|
|
129
|
+
break;
|
|
130
|
+
const full = path.join(root, file);
|
|
131
|
+
const entries = cachedTextByStat(closedSectionCache, full, full, (md) => actionableEntryRanges(md, section).slice(0, RECENT_CLOSED_MAX), (v) => v.slice())?.slice(0, room);
|
|
132
|
+
if (entries === undefined || entries.length === 0)
|
|
133
|
+
continue;
|
|
134
|
+
room -= entries.length;
|
|
135
|
+
listed.push(`${file} ## ${section} (newest ${entries.length})`);
|
|
136
|
+
for (const e of entries)
|
|
137
|
+
listed.push(`- ${e.start}-${e.end}: ${indexTitle(e.title)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (listed.length === 0)
|
|
141
|
+
return undefined;
|
|
142
|
+
return `Backlog index — each actionable entry, then the newest closed ones, with the 1-based line range it occupies in its file.
|
|
143
|
+
Read the entries you need by those ranges; the archive files (PLANS-DONE.md, BUGS-FIXED.md) grow
|
|
144
|
+
with every merge, so search them rather than reading them. Blocked parts of a multi-part series are
|
|
145
|
+
shown as one line per series.
|
|
146
|
+
<backlog-index>
|
|
147
|
+
${listed.join("\n")}
|
|
148
|
+
</backlog-index>`;
|
|
149
|
+
}
|