tumwater 0.1.2 → 0.2.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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** One loop's read-only inspection payload — the collector behind `tumwater role <id>`:
|
|
2
3
|
* what the loop IS (catalog entry or custom, enabled, paused, tier), what it RUNS ON
|
|
3
4
|
* (resolved provider/model, the budget fallback pair), what it CARRIES (the instructions
|
|
@@ -7,29 +8,59 @@
|
|
|
7
8
|
* without the CLI and one renderer consumes exactly this shape. Every read degrades like
|
|
8
9
|
* backlogPayload does — a missing tumwater.json is the defaults, a missing state file a
|
|
9
10
|
* fresh state, a missing queue directory an empty inbox — so the command works with the
|
|
10
|
-
*
|
|
11
|
-
import { defaultConfig, enabledRoleIds, isCustomRole, knownRoleIds, loadConfigSafe } from "../config/config.js";
|
|
11
|
+
* team stopped and never throws on a torn repo. */
|
|
12
|
+
import { defaultConfig, enabledRoleIds, isCustomRole, knownRoleIds, loadConfigSafe, parseJsonConfig } from "../config/config.js";
|
|
12
13
|
import { DIRECTOR_ROLE, customRole, roleById, roleTier, unknownRoleMessage } from "./roles.js";
|
|
13
14
|
import { queuedRolePromptCount } from "../inbox/inbox.js";
|
|
14
|
-
import { pausedRoles } from "../
|
|
15
|
+
import { pausedRoles } from "../team/team-state.js";
|
|
15
16
|
import { loadLoopState } from "../loop/loop-state.js";
|
|
17
|
+
import { baseRoleOf, loopIds, loopPaused } from "./loop-ids.js";
|
|
18
|
+
import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
19
|
+
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
20
|
+
import { quietHoursStatus } from "../scheduling/quiet-hours.js";
|
|
16
21
|
import { assembleTickPrompt } from "../tick/tick-prompt.js";
|
|
17
|
-
import { configForRole,
|
|
18
|
-
import {
|
|
22
|
+
import { configForRole, resolvedModelFields, reviewRunConfig, roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
|
|
23
|
+
import { fallbackPair, modelFallbackView } from "../config/config-fallbacks.js";
|
|
24
|
+
import { fallbackModelPair, piModelsPath, seamContextWindow } from "../pi/pi-models.js";
|
|
25
|
+
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
26
|
+
import { formatModelSelector } from "../config/model-selector.js";
|
|
27
|
+
import { roleImageInfo } from "../agent/image-input.js";
|
|
28
|
+
import { readImageProbeCache } from "../doctor/image-probe.js";
|
|
29
|
+
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
19
30
|
import { claudeWindowEntries, readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
20
|
-
import { roleNotesPath } from "../paths.js";
|
|
31
|
+
import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
|
|
21
32
|
import { readTextOrNull } from "../files/files.js";
|
|
33
|
+
import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
|
|
34
|
+
import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
|
|
35
|
+
import { canonicalRoleId } from "./legacy-role-ids.js";
|
|
36
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
37
|
+
/** The roles.<base>.<field> value the FILE actually holds, or undefined when the file never
|
|
38
|
+
* set that field. Needed only for the seeded per-role fields: overlayDefaults fills every
|
|
39
|
+
* built-in role's minRunIntervalSeconds from config.ts's catalog, so the merged config cannot
|
|
40
|
+
* tell an operator-set override from a seeded default. This reads the same raw file view
|
|
41
|
+
* unsetConfigKey judges presence against (configPath + CONFIG_BASENAME + objectOrEmpty), so
|
|
42
|
+
* the override tag and the drawer's clear path agree with what unsetting can actually remove. */
|
|
43
|
+
export function rawRoleField(root, roleId, field) {
|
|
44
|
+
const { raw } = parseJsonConfig(configPath(root), CONFIG_BASENAME);
|
|
45
|
+
const rawRoles = objectOrEmpty(raw).roles;
|
|
46
|
+
const entry = rawRoles && typeof rawRoles === "object" ? rawRoles[roleId] : undefined;
|
|
47
|
+
const obj = entry && typeof entry === "object" ? entry : undefined;
|
|
48
|
+
if (!obj)
|
|
49
|
+
return undefined;
|
|
50
|
+
return obj[field];
|
|
51
|
+
}
|
|
22
52
|
/** Collect one loop's inspection payload. `modelsPath` overrides pi's model definitions location
|
|
23
53
|
* — a test seam, like status/status-data.ts's snapshot — consulted only when a fallback pair is
|
|
24
54
|
* configured (the freeness verdict needs the definitions; nothing else does). */
|
|
25
|
-
export function rolePayload(root,
|
|
55
|
+
export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now = new Date()) {
|
|
56
|
+
const role = canonicalRoleId(typedRole);
|
|
26
57
|
// loadConfigSafe (not loadConfigCached's hold-last-good machinery — this is a one-shot
|
|
27
58
|
// command, not a poller): a broken or missing file degrades to the defaults, so the
|
|
28
59
|
// command still answers for built-in roles on a torn repo.
|
|
29
60
|
const { config } = loadConfigSafe(root);
|
|
30
61
|
const cfg = config ?? defaultConfig();
|
|
31
62
|
const catalog = roleById(role);
|
|
32
|
-
const customEntry = cfg.
|
|
63
|
+
const customEntry = cfg.customRoles.find((c) => c.name === role);
|
|
33
64
|
// The director is inspectable like any loop but has no catalog entry: a pseudo-Role whose
|
|
34
65
|
// find text is empty (rolePayload nulls it below — the director's queued prompts ARE its
|
|
35
66
|
// work), so the unknown-role throw only fires for ids nothing can answer.
|
|
@@ -44,20 +75,132 @@ export function rolePayload(root, role, modelsPath = piModelsPath()) {
|
|
|
44
75
|
throw new Error(unknownRoleMessage(role, knownRoleIds(cfg)));
|
|
45
76
|
}
|
|
46
77
|
const effective = configForRole(cfg, role);
|
|
47
|
-
// The claude billing answer, read
|
|
48
|
-
//
|
|
49
|
-
|
|
78
|
+
// The claude billing answer, read when a claude seam is in play — the role's OWN seam, or
|
|
79
|
+
// the review gate's when the reviewer runs claude (PLANS.md "Mixed claude + pi fleets, part
|
|
80
|
+
// 3/4" gap 7): a pi author on a claude reviewer still shows where its changes' spend lands.
|
|
81
|
+
// Its subscription window's primary type rides along for the renderer's label. The account
|
|
82
|
+
// file is team-global, so either seam reads the same answer.
|
|
83
|
+
const claudeInPlay = effective.agent === "claude" || (cfg.review.enabled && reviewRunConfig(cfg).agent === "claude");
|
|
84
|
+
const account = claudeInPlay ? readClaudeAccount(root) : null;
|
|
85
|
+
const info = readOrchestratorInfo(root);
|
|
86
|
+
const chainStep = (orchestratorAlive(root, info) && info?.chain?.[roleSeamTier(cfg, role)]) || null;
|
|
50
87
|
const billingWindow = account?.billing === "subscription" ? claudeWindowEntries(account)[0]?.type : undefined;
|
|
88
|
+
// pi's own saved default for a pi seam that resolves no model, read once beside the models
|
|
89
|
+
// catalog this collector already takes as a seam (piSettingsPath). The config's `thinking`
|
|
90
|
+
// wins over the default's because tumwater passes it to pi as `--thinking`, mirroring the
|
|
91
|
+
// snapshot's modelFromPi rule. Any other seam stays unchanged and carries no `piDefault`.
|
|
92
|
+
const runsPiWithoutAModel = (effective.agent === undefined || effective.agent === "pi") &&
|
|
93
|
+
effective.provider === undefined &&
|
|
94
|
+
effective.model === undefined;
|
|
95
|
+
const piDefault = runsPiWithoutAModel ? readPiDefaultModel(piSettingsPath(modelsPath), root) : null;
|
|
96
|
+
// The role's provider key and its cap (plans/provider-concurrency.md §8): the same pure
|
|
97
|
+
// key the orchestrator's admission resolves for this role, answered from the same
|
|
98
|
+
// piDefault seam the orchestrator carries, so `tumwater role <id>` names the exact key
|
|
99
|
+
// whose cap bounds the role. The cap is the map's entry for that key, absent when no
|
|
100
|
+
// `maxConcurrentPerProvider` map is set or the key has no entry (then the role is bounded
|
|
101
|
+
// only by the team total, and the cap is omitted).
|
|
102
|
+
const providerKey = seamConcurrencyKey(effective, piDefault);
|
|
103
|
+
const providerCap = providerKey !== undefined ? cfg.maxConcurrentPerProvider?.[providerKey] : undefined;
|
|
51
104
|
// The fallback story the operator sees: the declared pair, but the price-resolved pair when
|
|
52
105
|
// one exists (a map with no `default` entry borrows another tier's free pair) and the
|
|
53
106
|
// freeness verdict from the same read. `fallbackFree` false keeps the declared pair visible
|
|
54
107
|
// so a refused fallback is still named.
|
|
55
108
|
const declaredFallback = fallbackPair(cfg);
|
|
56
|
-
|
|
109
|
+
// The opencode catalog answer (part 6b/7), read the way the budget gate reads it — always,
|
|
110
|
+
// from the same team-global cache the startup gate seeds — so the fallback free-ness verdict
|
|
111
|
+
// prices opencode pairs against the same snapshot as the gate. A conditional read cannot
|
|
112
|
+
// decide up front which pair the verdict will price: `fallbackModelPair` may resolve to a
|
|
113
|
+
// free pair BORROWED from another tier (a map whose `default` entry is priced borrows a
|
|
114
|
+
// free pair elsewhere), which routes to opencode even when the role's own seam, the declared
|
|
115
|
+
// default pair, and the review gate are not opencode. A null catalog would read that borrowed
|
|
116
|
+
// opencode pair NOT free, so this view and the gate would disagree; reading always (as the
|
|
117
|
+
// gate does) keeps them on one answer. OpenCode's `catalog` is threaded the way claude's
|
|
118
|
+
// `account` is: absent, an opencode pair reads NOT free.
|
|
119
|
+
const opencodeCatalog = readOpencodeCache(root);
|
|
120
|
+
const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, readClaudeAccount(root), opencodeCatalog) : null;
|
|
57
121
|
const fallback = freeFallback ?? declaredFallback;
|
|
58
122
|
// One state read serves both the model-fallback episode and the next-tick preview below.
|
|
59
123
|
const state = loadLoopState(root, role);
|
|
60
124
|
const episode = state.modelFallback;
|
|
125
|
+
// The loop's recent tick outcomes, read from the event log (readEvents) so the command
|
|
126
|
+
// answers with the team stopped and never touches live state. readEvents returns the log's
|
|
127
|
+
// newest events in file (chronological) order, so filtering to this role's run_end events
|
|
128
|
+
// and taking the last 8 yields the newest 8, reversed to newest-first. Durations and
|
|
129
|
+
// summaries ride along only when the event carried them (a torn or empty line drops them),
|
|
130
|
+
// and an empty result is omitted so a never-ticked loop carries no `recentOutcomes` at all.
|
|
131
|
+
// One event-log read serves both per-loop facts — the recent outcomes and the spend sum — so
|
|
132
|
+
// the two cannot drift on what they cover: filtered to this role's run_end events once, then
|
|
133
|
+
// the outcome projection keeps the newest valid 8 while the usage fold sums the whole window.
|
|
134
|
+
const events = readEvents(root);
|
|
135
|
+
const tickEnds = events.filter((ev) => ev.type === "run_end" && eventRole(ev) === role);
|
|
136
|
+
// The event log is read without a shape filter, so a torn or hand-edited run_end line can
|
|
137
|
+
// carry a non-numeric ts/durationMs or an empty result — the same foreign shape every other
|
|
138
|
+
// consumer guards with finiteNumber. A line with no usable ts or result is as unusable as
|
|
139
|
+
// an absent one (the same "a torn line carries no usable timestamp" rule eventTick/tickStartMap
|
|
140
|
+
// apply), so it contributes no outcome; a non-numeric durationMs is dropped like a missing one.
|
|
141
|
+
const recentOutcomes = tickEnds
|
|
142
|
+
.filter((ev) => {
|
|
143
|
+
return finiteNumber(ev.ts, null) !== null && typeof ev.result === "string" && ev.result !== "";
|
|
144
|
+
})
|
|
145
|
+
.slice(-8)
|
|
146
|
+
.map((ev) => ({
|
|
147
|
+
ts: ev.ts,
|
|
148
|
+
result: ev.result,
|
|
149
|
+
...(finiteNumber(ev.durationMs, null) !== null ? { durationMs: ev.durationMs } : {}),
|
|
150
|
+
...(typeof ev.summary === "string" && ev.summary !== "" ? { summary: ev.summary } : {}),
|
|
151
|
+
}))
|
|
152
|
+
.reverse();
|
|
153
|
+
// The loop's own spend over the same retained window, summed per-run_end through the shared
|
|
154
|
+
// eventUsage guard (a torn or hand-edited run_end line contributes nothing: a non-numeric
|
|
155
|
+
// field reads as 0, and a line without a usable ts is as unusable as an absent one, matching
|
|
156
|
+
// recentOutcomes's drop rule so a torn tick never silently spends). Omitted when all three
|
|
157
|
+
// sums are 0, keeping a never-costly loop's JSON unchanged (the report's zero-means-absent
|
|
158
|
+
// convention).
|
|
159
|
+
const usage = tickEnds.reduce((acc, ev) => {
|
|
160
|
+
if (finiteNumber(ev.ts, null) === null)
|
|
161
|
+
return acc;
|
|
162
|
+
const u = eventUsage(ev);
|
|
163
|
+
acc.tokens += u.tokens;
|
|
164
|
+
acc.costUsd += u.costUsd;
|
|
165
|
+
acc.notionalCostUsd += u.notionalCostUsd;
|
|
166
|
+
return acc;
|
|
167
|
+
}, { tokens: 0, costUsd: 0, notionalCostUsd: 0 });
|
|
168
|
+
// The per-role daily cap, answered by the verdict the scheduler's gate shares (roleCapVerdicts)
|
|
169
|
+
// over the base role's instances, so the page cannot disagree with the hold. A cap of 0
|
|
170
|
+
// disables (roleCapPaused's rule) and the director is exempt, so neither carries the field.
|
|
171
|
+
const base = baseRoleOf(role);
|
|
172
|
+
// The roles.<base>.minRunIntervalSeconds override, resolved through the base role like the
|
|
173
|
+
// cap, quiet hours and instructions — an instance id shows (and can edit) the override its
|
|
174
|
+
// runs carry. Presence is judged against the FILE (rawRoleField), not the merged cfg:
|
|
175
|
+
// overlayDefaults seeds every built-in role's minRunIntervalSeconds from config.ts's catalog,
|
|
176
|
+
// so the merged value would make steward/qa/... always look overridden and the drawer's
|
|
177
|
+
// clear a no-op. undefined when unset, so the spread below drops it (the payload stays
|
|
178
|
+
// byte-identical when there is no override).
|
|
179
|
+
const rawInterval = rawRoleField(root, base, "minRunIntervalSeconds");
|
|
180
|
+
const minRunIntervalOverride = typeof rawInterval === "number" ? rawInterval : undefined;
|
|
181
|
+
// The raw provider/model/thinking the FILE holds (roles.<base>), distinct from the resolved
|
|
182
|
+
// provider/model/thinking fields configForRole merges with the top level — the drawer's
|
|
183
|
+
// Model overrides editor prefills from these, empty when unset. Presence is judged against
|
|
184
|
+
// the FILE (rawRoleField) like minRunIntervalOverride, so a config that relies on the top
|
|
185
|
+
// level or a tier name carries no override. Each is spread only when a string, so a file
|
|
186
|
+
// with no overrides yields a payload byte-identical to today's.
|
|
187
|
+
const providerOverrideRaw = rawRoleField(root, base, "provider");
|
|
188
|
+
const modelOverrideRaw = rawRoleField(root, base, "model");
|
|
189
|
+
const thinkingOverrideRaw = rawRoleField(root, base, "thinking");
|
|
190
|
+
const providerOverride = typeof providerOverrideRaw === "string" ? providerOverrideRaw : undefined;
|
|
191
|
+
const modelOverride = typeof modelOverrideRaw === "string" ? modelOverrideRaw : undefined;
|
|
192
|
+
const thinkingOverride = typeof thinkingOverrideRaw === "string" ? thinkingOverrideRaw : undefined;
|
|
193
|
+
const capUsd = cfg.maxDailyCostUsdPerRole?.[base];
|
|
194
|
+
let dailyCap;
|
|
195
|
+
if (role !== DIRECTOR_ROLE && typeof capUsd === "number" && Number.isFinite(capUsd) && capUsd > 0) {
|
|
196
|
+
const ids = loopIds(cfg).filter((id) => baseRoleOf(id) === base);
|
|
197
|
+
if (!ids.includes(role))
|
|
198
|
+
ids.push(role);
|
|
199
|
+
const runners = ids.map((id) => ({ role: id, state: id === role ? state : loadLoopState(root, id) }));
|
|
200
|
+
const verdict = roleCapVerdicts(runners, cfg.maxDailyCostUsdPerRole, now.getTime()).get(base);
|
|
201
|
+
if (verdict)
|
|
202
|
+
dailyCap = { capUsd, spentUsd: verdict.spentUsd, over: verdict.over, roleIds: verdict.roleIds };
|
|
203
|
+
}
|
|
61
204
|
// The role's notebook, read for display. The same degradation as every other read here: a
|
|
62
205
|
// missing, empty, or unreadable file is simply "no note yet". The director has none.
|
|
63
206
|
let note = null;
|
|
@@ -65,26 +208,68 @@ export function rolePayload(root, role, modelsPath = piModelsPath()) {
|
|
|
65
208
|
const text = readTextOrNull(roleNotesPath(root, role));
|
|
66
209
|
note = text !== null && text.trim() !== "" ? text : null;
|
|
67
210
|
}
|
|
211
|
+
// The windows the scheduler applies (gates/gate-polls.ts), through the same parser and
|
|
212
|
+
// predicate; the director is exempt from both gates.
|
|
213
|
+
const quietHours = [];
|
|
214
|
+
if (role !== DIRECTOR_ROLE) {
|
|
215
|
+
const own = quietHoursStatus(cfg.quietHoursPerRole?.[baseRoleOf(role)], now);
|
|
216
|
+
if (own.window !== null)
|
|
217
|
+
quietHours.push({ scope: "role", window: own.window, inWindow: own.inWindow });
|
|
218
|
+
const team = quietHoursStatus(cfg.quietHours, now);
|
|
219
|
+
if (team.window !== null)
|
|
220
|
+
quietHours.push({ scope: "fleet", window: team.window, inWindow: team.inWindow }); // vocabulary-ok: persisted --json scope value
|
|
221
|
+
}
|
|
222
|
+
// The resolved primary seam's declared context window. A run currently on the model
|
|
223
|
+
// fallback may use a different window, which is the fallback's own documented state.
|
|
224
|
+
const contextWindow = seamContextWindow(configForRole(cfg, role), root, modelsPath);
|
|
68
225
|
return {
|
|
69
226
|
id: role,
|
|
70
227
|
title: resolved.title,
|
|
71
228
|
custom: isCustomRole(cfg, role),
|
|
72
229
|
enabled: enabledRoleIds(cfg).includes(role),
|
|
73
|
-
paused: pausedRoles(root)
|
|
230
|
+
paused: loopPaused(new Set(pausedRoles(root)), role),
|
|
74
231
|
tier: roleTier(role),
|
|
75
232
|
modelTier: roleSeamTier(cfg, role),
|
|
76
233
|
...resolvedModelFields(effective.provider, effective.model, effective.thinking),
|
|
77
|
-
|
|
234
|
+
...(piDefault
|
|
235
|
+
? {
|
|
236
|
+
piDefault: formatModelSelector({
|
|
237
|
+
...piDefault,
|
|
238
|
+
thinking: effective.thinking ?? piDefault.thinking,
|
|
239
|
+
}),
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
backend: effective.agent ?? "pi",
|
|
243
|
+
...(providerKey !== undefined ? { providerKey } : {}),
|
|
244
|
+
...(providerCap !== undefined ? { providerCap } : {}),
|
|
78
245
|
fallback,
|
|
79
246
|
fallbackFree: freeFallback !== null,
|
|
80
247
|
modelFallback: modelFallbackView(episode, cfg, role),
|
|
81
248
|
...(account !== null ? { billing: account.billing } : {}),
|
|
82
249
|
...(billingWindow !== undefined ? { billingWindow } : {}),
|
|
83
|
-
|
|
84
|
-
|
|
250
|
+
chainStep,
|
|
251
|
+
...roleImageInfo(cfg, role, modelsPath, readImageProbeCache(root)),
|
|
252
|
+
minRunIntervalSeconds: effective.minRunIntervalSeconds,
|
|
253
|
+
...(minRunIntervalOverride !== undefined ? { minRunIntervalOverride } : {}),
|
|
254
|
+
...(providerOverride !== undefined ? { providerOverride } : {}),
|
|
255
|
+
...(modelOverride !== undefined ? { modelOverride } : {}),
|
|
256
|
+
...(thinkingOverride !== undefined ? { thinkingOverride } : {}),
|
|
257
|
+
// The configured parallel-runner count (roles.<base>.instances), present only when the
|
|
258
|
+
// config sets it — unlike minRunIntervalSeconds no catalog seed fills built-ins' counts,
|
|
259
|
+
// so the merged cfg tells presence exactly. Resolved through the base role like the
|
|
260
|
+
// instructions and min-run-interval override.
|
|
261
|
+
...(cfg.roles[base]?.instances !== undefined ? { instances: cfg.roles[base].instances } : {}),
|
|
262
|
+
// The roles.<base>.instructions override verbatim — resolved through the base role like
|
|
263
|
+
// the cap and quiet hours, so an instance id shows (and can edit) the override its runs
|
|
264
|
+
// carry (tick-prompt reads config.roles[base]); null when unset.
|
|
265
|
+
instructions: cfg.roles[baseRoleOf(role)]?.instructions ?? null,
|
|
85
266
|
find: role === DIRECTOR_ROLE ? null : resolved.find,
|
|
86
|
-
note,
|
|
267
|
+
notes: note,
|
|
87
268
|
inboxCount: queuedRolePromptCount(root, role),
|
|
269
|
+
// The resolved primary seam's declared context window in tokens, or null when it cannot
|
|
270
|
+
// be known (a non-pi seam, an unresolvable provider/model pair, or a model with no
|
|
271
|
+
// declared window) — the same seam `- Next run:` previews. Independent of the fallback.
|
|
272
|
+
contextWindow,
|
|
88
273
|
// The preview seam: the queued prompt (if any) is read, not consumed, so an inspection
|
|
89
274
|
// never costs the loop its queued work — the property the tick-prompt test pins.
|
|
90
275
|
nextPrompt: assembleTickPrompt({
|
|
@@ -93,6 +278,20 @@ export function rolePayload(root, role, modelsPath = piModelsPath()) {
|
|
|
93
278
|
role,
|
|
94
279
|
state,
|
|
95
280
|
preview: true,
|
|
281
|
+
// The compact prompt every run gets (loop-tick.ts), so the preview is the real prompt.
|
|
282
|
+
compact: true,
|
|
96
283
|
})?.prompt ?? null,
|
|
284
|
+
// The loop's schedule — surfaced read-only from the same persisted state rolePayload
|
|
285
|
+
// already loaded for the model-fallback episode and the next-tick preview, so no new
|
|
286
|
+
// read path exists and the collector stays one loadLoopState per role. `lastRunEndedAt`
|
|
287
|
+
// is omitted (JSON unchanged) until the loop has actually ticked.
|
|
288
|
+
nextRunAt: state.nextRunAt,
|
|
289
|
+
backoffSeconds: state.backoffSeconds,
|
|
290
|
+
yieldMultiplier: yieldMultiplierFor(state),
|
|
291
|
+
...(state.lastRunEndedAt !== undefined ? { lastRunEndedAt: state.lastRunEndedAt } : {}),
|
|
292
|
+
...(quietHours.length > 0 ? { quietHours } : {}),
|
|
293
|
+
...(recentOutcomes.length > 0 ? { recentOutcomes } : {}),
|
|
294
|
+
...(usage.tokens !== 0 || usage.costUsd !== 0 || usage.notionalCostUsd !== 0 ? { usage } : {}),
|
|
295
|
+
...(dailyCap ? { dailyCap } : {}),
|
|
97
296
|
};
|
|
98
297
|
}
|
package/dist/src/roles/roles.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The role registries and classification helpers over the built-in role catalog. The catalog
|
|
2
3
|
* itself — each loop's identity and its "find something to do" prompt text — lives in
|
|
3
4
|
* role-catalog.ts (split out 2026-10-01, organize); this module re-exports ROLES and Role so
|
|
@@ -10,24 +11,25 @@ import { typoSuffix } from "../text/suggest.js";
|
|
|
10
11
|
export { ROLES };
|
|
11
12
|
/** The director loop's id — a target that is not in ROLES (allRoleIds appends it) and is
|
|
12
13
|
* driven by operator prompts, not a find prompt: an un-targeted `tumwater prompt` queues
|
|
13
|
-
* there. It keeps running through a
|
|
14
|
+
* there. It keeps running through a team-wide `pause` (which stops only the role loops);
|
|
14
15
|
* an un-targeted `stop` signals the whole orchestrator, director included. */
|
|
15
16
|
export const DIRECTOR_ROLE = "director";
|
|
16
17
|
/** The bugfix loop's id. It is the one work role that also defers like a maintenance role while its
|
|
17
|
-
* backlog (BUGS.md `## Open`) is empty — see deferTick in src/scheduling/scheduling.ts.
|
|
18
|
+
* backlog (BUGS.md `## Open`) is empty — see deferTick in src/scheduling/scheduling.ts. With open
|
|
19
|
+
* bugs it never defers, but pollRunnerReasons stands it down while all of them are held. */
|
|
18
20
|
export const BUGFIX_ROLE = "bugfix";
|
|
19
21
|
/** Work-tier roles (need-based prioritization): they
|
|
20
|
-
* ship work — feature and bugfix
|
|
22
|
+
* ship work — feature and bugfix merge code on main, plan feeds them — so their due ticks are
|
|
21
23
|
* never deferred and slot allocation always orders them ahead of maintenance. */
|
|
22
24
|
const WORK_ROLES = new Set(["feature", "bugfix", "plan"]);
|
|
23
25
|
/** Observer roles (plans/observer-roles.md 1/2): a role whose product is an observation, not a
|
|
24
26
|
* commit, and for which `no_change` means "checked, all well" rather than "found nothing to do".
|
|
25
27
|
* The idle ladder's premise — a loop that keeps finding nothing stops burning model time — does
|
|
26
|
-
* not hold for these, so their no_change tick schedules at `
|
|
28
|
+
* not hold for these, so their no_change tick schedules at `minRunIntervalSeconds` and leaves
|
|
27
29
|
* `backoffSeconds` at 0 (src/scheduling/backoff.ts). The error ladder still applies in full, and
|
|
28
30
|
* they are removed from DEFERRABLE_ROLES because their input (the running product for `qa`, the
|
|
29
|
-
* event log for `
|
|
30
|
-
export const OBSERVER_ROLES = new Set(["qa", "
|
|
31
|
+
* event log for `selfcheck`) is not a function of whether main moved. */
|
|
32
|
+
export const OBSERVER_ROLES = new Set(["qa", "selfcheck"]);
|
|
31
33
|
/** The built-in maintenance roles that author code on their own cadence — the eight members
|
|
32
34
|
* DEFERRABLE_ROLES and BASELINE_BLOCKED_ROLES share. Both sets list them because both charter
|
|
33
35
|
* the same roles (defer-until-needed scheduling, red-main blocking), so the list lives once
|
|
@@ -36,15 +38,16 @@ const CODE_MAINTENANCE_ROLES = [
|
|
|
36
38
|
"organize",
|
|
37
39
|
"coverage",
|
|
38
40
|
"clean",
|
|
39
|
-
"
|
|
41
|
+
"dedupe",
|
|
40
42
|
"perf",
|
|
41
43
|
"security",
|
|
42
44
|
"robustness",
|
|
43
45
|
"improve",
|
|
44
46
|
];
|
|
45
47
|
/** Maintenance-tier roles (need-based prioritization): exactly the ten built-ins whose due
|
|
46
|
-
* ticks are deferrable while no feature
|
|
47
|
-
* their last tick and that tick did nothing. Unknown/custom roles are
|
|
48
|
+
* ticks are deferrable while no feature, bugfix, director, operator or human commit has merged
|
|
49
|
+
* into main since their last tick and that tick did nothing. Unknown/custom roles are
|
|
50
|
+
* deliberately NOT in this
|
|
48
51
|
* set — the harness cannot judge what an arbitrary custom role needs, so they never defer (they
|
|
49
52
|
* still sort into tier 1 for fairOrder via roleTier). Observers are excluded: an unmoved tree
|
|
50
53
|
* says nothing about whether the product or the event log has something new to report. */
|
|
@@ -78,8 +81,8 @@ export function roleTier(role) {
|
|
|
78
81
|
* by the gate's pre-check, so spending an authoring run on it is pure waste. Exempt: director
|
|
79
82
|
* (human prompts outrank autonomous gates, like the budget gate), bugfix (the designated healer
|
|
80
83
|
* — its tick runs the suite per "leave the project working" and can fix a red main through the
|
|
81
|
-
* existing pre-merge gate; blocking it would leave only humans able to unblock the
|
|
82
|
-
* plan/readme/steward/qa/
|
|
84
|
+
* existing pre-merge gate; blocking it would leave only humans able to unblock the team), and
|
|
85
|
+
* plan/readme/steward/qa/selfcheck (markdown-only charter — their diffs are exempt from the build
|
|
83
86
|
* pre-check via review.exemptPaths, so a red main does not block them). */
|
|
84
87
|
export const BASELINE_BLOCKED_ROLES = new Set([
|
|
85
88
|
"feature",
|
|
@@ -90,8 +93,8 @@ export const BASELINE_BLOCKED_ROLES = new Set([
|
|
|
90
93
|
export function baselineBlocked(role) {
|
|
91
94
|
return BASELINE_BLOCKED_ROLES.has(baseRoleOf(role));
|
|
92
95
|
}
|
|
93
|
-
/** Work ratio 4/4: which commit tier a loop's base role
|
|
94
|
-
* split the report and the "
|
|
96
|
+
/** Work ratio 4/4: which commit tier a loop's base role merges into, for the work vs maintenance
|
|
97
|
+
* split the report and the "Merged today" tile show and the maintenance allowance (Work ratio
|
|
95
98
|
* 1a/4, src/gates/maintenance-quota.ts) counts. The one home of the split. `work` is the
|
|
96
99
|
* shipping tier (feature, bugfix, director); `maintenance` is code upkeep plus readme. Every
|
|
97
100
|
* other role (plan, steward, observers, custom) is neither and counts only toward the total
|
|
@@ -129,7 +132,7 @@ export function unknownRoleMessage(role, validIds) {
|
|
|
129
132
|
* role-specific find-something-to-do text, and the title is what identifies the loop inside
|
|
130
133
|
* its own prompt (`You are the "<name>" loop (user-defined loop)`) and commit context.
|
|
131
134
|
* Custom loops run the `default` tier — the harness cannot judge what an arbitrary loop's
|
|
132
|
-
* work needs, so it gets the
|
|
135
|
+
* work needs, so it gets the team's ordinary model. */
|
|
133
136
|
export function customRole(name, task) {
|
|
134
|
-
return { id: name, title: "
|
|
137
|
+
return { id: name, title: "custom role", find: task, tier: "default" };
|
|
135
138
|
}
|
|
@@ -11,12 +11,12 @@ import { yieldScaledRole } from "../roles/roles.js";
|
|
|
11
11
|
* unit-tested without touching a disk. */
|
|
12
12
|
/** Clear a loop's backoff so its next orchestrator poll finds it immediately due: zero
|
|
13
13
|
* backoffSeconds (so the next no_change/error tick restarts the backoff ladder from the
|
|
14
|
-
* bottom instead of climbing from wherever the
|
|
14
|
+
* bottom instead of climbing from wherever the team parked) and pull nextRunAt forward to
|
|
15
15
|
* now. Pure: returns a new state and preserves everything else — counters (an
|
|
16
16
|
* observation-window reset is zeroCounters' job), wake tracking (lastMainHead), and the
|
|
17
17
|
* last-result fields. Scheduling operation, not observation-window reset: it exists so an
|
|
18
18
|
* operator who fixed what the loops were failing on can say "try again" (BUGS.md
|
|
19
|
-
* 2026-09-15: the
|
|
19
|
+
* 2026-09-15: the team had no wake lever). Also stamps wokenAt, which is what lets
|
|
20
20
|
* isEligible honor the demand over the role's min-tick interval (LoopState.wokenAt). */
|
|
21
21
|
export function clearBackoff(s, now) {
|
|
22
22
|
// wokenAt marks the wake as newer than the current gap window's opening tick, which is
|
|
@@ -27,7 +27,7 @@ export function clearBackoff(s, now) {
|
|
|
27
27
|
}
|
|
28
28
|
/** Re-apply a wake that was consumed while the just-ended tick was still in flight. The tick
|
|
29
29
|
* holds the same state object wake() mutated in place, but applyTickOutcome's own schedule
|
|
30
|
-
* overwrites the wake:
|
|
30
|
+
* overwrites the wake: lastRunEndedAt is re-stamped past wokenAt (so the min-gap exemption
|
|
31
31
|
* reads stale) and nextRunAt is scheduled a fresh gap or backoff out — the operator's "try
|
|
32
32
|
* again now" silently waits out the whole interval. Called by the tick's end-save after
|
|
33
33
|
* applyTickOutcome (src/tick/tick-apply.ts), it re-applies the demand exactly like a wake
|
|
@@ -40,22 +40,22 @@ export function clearBackoff(s, now) {
|
|
|
40
40
|
export function restoreMidTickWake(s) {
|
|
41
41
|
if (s.resumePending)
|
|
42
42
|
return false;
|
|
43
|
-
if (s.wokenAt === undefined || s.wokenAt <= (s.
|
|
43
|
+
if (s.wokenAt === undefined || s.wokenAt <= (s.lastRunStartedAt ?? 0))
|
|
44
44
|
return false;
|
|
45
45
|
// clearBackoff stamps wokenAt = now, which must read NEWER than the end-save's
|
|
46
|
-
//
|
|
46
|
+
// lastRunEndedAt for isEligible's exemption to fire — both are Date.now() reads, so a
|
|
47
47
|
// same-millisecond tie would swallow the demand; floor it just past the end stamp.
|
|
48
|
-
Object.assign(s, clearBackoff(s, Math.max(Date.now(), (s.
|
|
48
|
+
Object.assign(s, clearBackoff(s, Math.max(Date.now(), (s.lastRunEndedAt ?? 0) + 1)));
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
51
|
/** Yield-scaled clocks: how long a role's own recent
|
|
52
52
|
* results stretch its min-tick gap. The ring (LoopState.recentOutcomes) holds one char per
|
|
53
|
-
* COUNTED tick — a
|
|
53
|
+
* COUNTED tick — a merge (`changed`/`queued`) is `L`, every other counted result is `n`;
|
|
54
54
|
* the error class (`error`/`aborted`/`quiet_killed`) is no yield evidence either way and is
|
|
55
55
|
* not recorded at all, so a run of backend failures neither stretches a role's clock nor
|
|
56
56
|
* resets it. */
|
|
57
57
|
export const YIELD_RING = 20;
|
|
58
|
-
const
|
|
58
|
+
const YIELD_MERGE = "L";
|
|
59
59
|
/** Results that never enter the ring. */
|
|
60
60
|
const UNCOUNTED_RESULTS = new Set(["error", "aborted", "quiet_killed"]);
|
|
61
61
|
/** Record one finished tick's result on the state's yield ring, oldest entries falling off
|
|
@@ -64,21 +64,21 @@ const UNCOUNTED_RESULTS = new Set(["error", "aborted", "quiet_killed"]);
|
|
|
64
64
|
export function pushYieldOutcome(s, result) {
|
|
65
65
|
if (UNCOUNTED_RESULTS.has(result))
|
|
66
66
|
return;
|
|
67
|
-
const ch = result === "changed" || result === "queued" ?
|
|
67
|
+
const ch = result === "changed" || result === "queued" ? YIELD_MERGE : "n";
|
|
68
68
|
s.recentOutcomes = ((s.recentOutcomes ?? "") + ch).slice(-YIELD_RING);
|
|
69
69
|
}
|
|
70
70
|
const YIELD_MAX = 8;
|
|
71
|
-
/** The multiplier on a scalable role's
|
|
72
|
-
* 1 while any of the last 10 counted ticks
|
|
71
|
+
/** The multiplier on a scalable role's minRunIntervalSeconds gap its recent yield earns:
|
|
72
|
+
* 1 while any of the last 10 counted ticks merged, otherwise doubling per 5 further empty
|
|
73
73
|
* ticks — 10 empties ×2, 15 ×4, 20 ×8 — capped at 8. `recent` is the ring's chars, oldest
|
|
74
74
|
* first, as pushYieldOutcome recorded them. Pure; unit-tested beside the other ladder math.
|
|
75
|
-
* A
|
|
76
|
-
* the ring: one
|
|
77
|
-
* half only matters once the
|
|
75
|
+
* A merge inside the last 10 resets the multiplier to 1 even when older empties remain in
|
|
76
|
+
* the ring: one merge is the evidence the role's clock should trust, and the ring's older
|
|
77
|
+
* half only matters once the merge has aged out of the recent window. */
|
|
78
78
|
export function yieldMultiplier(recent) {
|
|
79
|
-
if (recent.slice(-10).includes(
|
|
79
|
+
if (recent.slice(-10).includes(YIELD_MERGE))
|
|
80
80
|
return 1;
|
|
81
|
-
const empty = recent.filter((c) => c !==
|
|
81
|
+
const empty = recent.filter((c) => c !== YIELD_MERGE).length;
|
|
82
82
|
if (empty < 10)
|
|
83
83
|
return 1;
|
|
84
84
|
return Math.min(YIELD_MAX, 2 ** (Math.floor((empty - 10) / 5) + 1));
|
|
@@ -92,6 +92,16 @@ export function yieldMultiplier(recent) {
|
|
|
92
92
|
export function yieldMultiplierFor(s) {
|
|
93
93
|
return yieldScaledRole(s.role) ? yieldMultiplier((s.recentOutcomes ?? "").split("")) : 1;
|
|
94
94
|
}
|
|
95
|
+
/** The `×N` yield-multiplier suffix a next-run display rides: `×4` when the multiplier is
|
|
96
|
+
* above 1, else `""` — the quiet scalable loop's stretched effective gap made visible (the
|
|
97
|
+
* multiplier gates the scheduled gap in isEligible, so a `now`-due cell at ×4 is a role
|
|
98
|
+
* waiting out its gap, not a broken clock). Shared by the status table's `next run` cell
|
|
99
|
+
* (status-render.ts's nextRunCell) and the role view's Schedule line (role-render.ts) so the
|
|
100
|
+
* two node surfaces cannot drift on how an above-1 multiplier is worded; the GUI renders the
|
|
101
|
+
* same rule from its own separate-runtime copy (gui-client.ts's fmtNextRun). */
|
|
102
|
+
export function yieldMultiplierSuffix(mult) {
|
|
103
|
+
return mult > 1 ? ` ×${mult}` : "";
|
|
104
|
+
}
|
|
95
105
|
/** Next step of a backoff ladder: initial (capped) on the first step, then multiplied, capped. */
|
|
96
106
|
export function nextBackoffSeconds(current, ladder) {
|
|
97
107
|
const { initialSeconds, factor, maxSeconds } = ladder;
|
|
@@ -108,16 +118,16 @@ export function scheduleBackoff(s, ladder) {
|
|
|
108
118
|
s.nextRunAt = Date.now() + s.backoffSeconds * 1000;
|
|
109
119
|
}
|
|
110
120
|
/** Schedule the loop's next tick at its minimum interval — the role-resolved config's
|
|
111
|
-
*
|
|
121
|
+
* minRunIntervalSeconds (a per-role slow clock), converted seconds→ms here so every outcome
|
|
112
122
|
* arm that shares this cadence cannot drift apart. The productive-side counterpart of
|
|
113
123
|
* scheduleBackoff: together the two own the whole "when does this loop run again" decision. */
|
|
114
124
|
export function scheduleAtMinInterval(s, cfg) {
|
|
115
|
-
s.nextRunAt = Date.now() + cfg.
|
|
125
|
+
s.nextRunAt = Date.now() + cfg.minRunIntervalSeconds * 1000;
|
|
116
126
|
}
|
|
117
127
|
/** Backoff ladder for failed ticks (`error` results). The idle ladder prices hour-long model
|
|
118
128
|
* runs — its cap exists so a loop that keeps finding nothing stops burning model time. A tick
|
|
119
129
|
* that fails (a broken toolchain, a dead pi subprocess) often never reaches the model, so it
|
|
120
|
-
* climbs this short ladder, capped in minutes: one broken `git` must not park a
|
|
130
|
+
* climbs this short ladder, capped in minutes: one broken `git` must not park a team for the
|
|
121
131
|
* idle ladder's 10-hour sleep (BUGS.md, the 2026-09-15 outage). One ladder, one sensible
|
|
122
132
|
* default, no knob: the cap is the point. */
|
|
123
133
|
export const ERROR_BACKOFF = { initialSeconds: 30, factor: 2, maxSeconds: 600 };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Work-instance claims (plans/parallel-work-instances.md "Claims", part 4/7): which backlog
|
|
2
3
|
* entry each runner of a multi-instance base role may take, whether it must drop the entry, and
|
|
3
4
|
* which entry a staged diff moved. The pure policies live here — assignment is the first free
|
|
4
5
|
* entry in file order, release is `left` / `ineligible` / `disabled` / `stale` — so
|
|
5
6
|
* orchestrator-scheduling.ts and tick-stage.ts share one definition and the rules are unit
|
|
6
|
-
* tested without a
|
|
7
|
+
* tested without a team. The claim itself is persisted on LoopState, so it survives a crash,
|
|
7
8
|
* resume, revision and leftover recovery. */
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { hasOutstandingWork } from "../loop/loop-state.js";
|
|
10
|
-
import { eligibleEntries, entryKey } from "../backlog/backlog-eligibility.js";
|
|
11
|
+
import { backlogHolds, eligibleEntries, entryKey } from "../backlog/backlog-eligibility.js";
|
|
11
12
|
import { actionableEntryRanges, actionableEntryRangesFor } from "../backlog/backlog-structure.js";
|
|
12
13
|
import { readTextOrNull } from "../files/files.js";
|
|
13
14
|
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
@@ -24,6 +25,15 @@ export const CLAIM_IDLE_MAX_MS = 24 * 60 * 60 * 1000;
|
|
|
24
25
|
export function planBacklogTarget(config) {
|
|
25
26
|
return configuredInstances(config, "feature") + 1;
|
|
26
27
|
}
|
|
28
|
+
/** Does PLANS.md already hold the plan loop's target (planBacklogTarget eligible plans), with no
|
|
29
|
+
* plan marked needs-review or needs-replan? The charter makes splitting or replanning a marked
|
|
30
|
+
* plan outrank the stop, so a marked entry keeps the plan loop running; otherwise a plan tick can
|
|
31
|
+
* only end in TUMWATER_NOTHING_TO_DO and the scheduler defers it (deferTickReason). */
|
|
32
|
+
export function planBacklogFull(root, config) {
|
|
33
|
+
if (eligibleEntries(root, "feature").length < planBacklogTarget(config))
|
|
34
|
+
return false;
|
|
35
|
+
return !backlogHolds(root).plans.some((h) => h === "needs-review" || h === "needs-replan");
|
|
36
|
+
}
|
|
27
37
|
/** The backlog file and section a role's entries live under: PLANS.md's `## Planned` for
|
|
28
38
|
* feature, BUGS.md's `## Open` for bugfix. An instance id resolves through its base role. */
|
|
29
39
|
function roleSection(role) {
|
|
@@ -59,15 +69,15 @@ export function assignNext(free) {
|
|
|
59
69
|
return free[0] ?? null;
|
|
60
70
|
}
|
|
61
71
|
/** Why a runner must drop its claim, or null when it keeps it. A runner is idle when it is not
|
|
62
|
-
* running, has no queued
|
|
63
|
-
* and `stale` only release an idle runner, while a claim whose entry left or turned
|
|
64
|
-
* is released outright (there is nothing left to do). */
|
|
72
|
+
* running, has no queued merge, no outstanding revision and no pending resume; `disabled`,
|
|
73
|
+
* `paused` and `stale` only release an idle runner, while a claim whose entry left or turned
|
|
74
|
+
* ineligible is released outright (there is nothing left to do). */
|
|
65
75
|
export function claimReleaseReason(runner, ctx) {
|
|
66
76
|
const claim = runner.state.claim;
|
|
67
77
|
if (!claim)
|
|
68
78
|
return null;
|
|
69
79
|
const idle = !runner.state.running &&
|
|
70
|
-
!ctx.
|
|
80
|
+
!ctx.hasQueuedMerge &&
|
|
71
81
|
!hasOutstandingWork(runner.state);
|
|
72
82
|
if (!ctx.listedKeys.has(claim.key))
|
|
73
83
|
return "left";
|
|
@@ -75,12 +85,14 @@ export function claimReleaseReason(runner, ctx) {
|
|
|
75
85
|
return "ineligible";
|
|
76
86
|
if (!ctx.enabled && idle)
|
|
77
87
|
return "disabled";
|
|
88
|
+
if (ctx.paused && idle)
|
|
89
|
+
return "paused";
|
|
78
90
|
if (idle && ctx.now - claim.at > CLAIM_IDLE_MAX_MS)
|
|
79
91
|
return "stale";
|
|
80
92
|
return null;
|
|
81
93
|
}
|
|
82
94
|
/** The entries `headMd` moved out of `section` that `baseMd` still listed, in base file order.
|
|
83
|
-
* A moved entry's key matches whether it
|
|
95
|
+
* A moved entry's key matches whether it merged into `## Done`/`## Fixed` or was renamed; a key
|
|
84
96
|
* absent from the head section means it is no longer actionable there. */
|
|
85
97
|
export function movedOutEntries(baseMd, headMd, section) {
|
|
86
98
|
const headKeys = new Set(actionableEntryRanges(headMd, section).map((e) => entryKey(e.title)));
|
|
@@ -102,7 +114,7 @@ export async function stagedMovedEntries(wt, mainBranch, role) {
|
|
|
102
114
|
/** How many permits to withhold from tier ≥ 1 acquirers this poll so a work loop that has work
|
|
103
115
|
* to take can claim one without waiting out a maintenance tick (plans/parallel-work-instances.md
|
|
104
116
|
* "Priority headroom", part 7/7). Only the claim-taking roles (feature, bugfix) count: an idle
|
|
105
|
-
* one — not running, with no queued
|
|
117
|
+
* one — not running, with no queued merge — counts when it either holds an eligible claim or
|
|
106
118
|
* its base role still has an unclaimed eligible entry. Returns min(floor(maxConcurrent / 3),
|
|
107
119
|
* the count), so an entirely busy work tier reserves nothing. */
|
|
108
120
|
export function workReserveCount(root, runners, opts) {
|
|
@@ -137,7 +149,11 @@ export function workReserveCount(root, runners, opts) {
|
|
|
137
149
|
continue;
|
|
138
150
|
if (!opts.enabled.has(r.role))
|
|
139
151
|
continue;
|
|
140
|
-
if (r.state.running || opts.
|
|
152
|
+
if (r.state.running || opts.queuedMergeRoles.has(r.role))
|
|
153
|
+
continue;
|
|
154
|
+
// A work loop whose provider is full (capped and at its cap) cannot use a reserved permit
|
|
155
|
+
// anyway, so it must not withhold one from another provider's maintenance tick (§6).
|
|
156
|
+
if (opts.providerHasHeadroom !== undefined && !opts.providerHasHeadroom(r))
|
|
141
157
|
continue;
|
|
142
158
|
const eligible = eligibleFor(base);
|
|
143
159
|
const holdsClaim = r.state.claim !== undefined && eligible.has(r.state.claim.key);
|