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,35 +1,50 @@
|
|
|
1
|
-
import { commitAll } from "../git/git.js";
|
|
1
|
+
import { commitAll, foldCommitsSince, headOf } from "../git/git.js";
|
|
2
2
|
import { changedFiles } from "../git/git-diff.js";
|
|
3
|
-
import { buildCommitMessage, commitTrailer, extractCommitBody, extractSummary, fallbackSummary, formatCommitBody, mergeCommitBody, stampedSubject, } from "../git/commit-message.js";
|
|
3
|
+
import { buildCommitMessage, commitTrailer, extractCommitBody, extractSummary, summaryOverCap, fallbackSummary, formatCommitBody, mergeCommitBody, stampedSubject, } from "../git/commit-message.js";
|
|
4
4
|
import { logEventBestEffort } from "../events/events.js";
|
|
5
|
-
import {
|
|
5
|
+
import { enqueueMerge, entryRevisionFields } from "../merge/merge-queue.js";
|
|
6
6
|
import { plural } from "../text/phrases.js";
|
|
7
7
|
import { recordFlow } from "./qa-coverage.js";
|
|
8
8
|
import { INSTANCE_ROLES, baseRoleOf, configuredInstances } from "../roles/loop-ids.js";
|
|
9
9
|
import { stagedMovedEntries } from "../scheduling/claims.js";
|
|
10
10
|
import { entryKey } from "../backlog/backlog-eligibility.js";
|
|
11
11
|
import { assignedMovedFinding, heldMovedFinding } from "./stage-check.js";
|
|
12
|
-
|
|
12
|
+
import { archiveNewFinishedEntries } from "../backlog/backlog-archive.js";
|
|
13
|
+
/** Fold commits an agent run made since `base` into the tick's tree, keeping the harness commit
|
|
14
|
+
* the tick's only one (INV-H1); warns when any were folded. The one home of the fold-and-warn
|
|
15
|
+
* policy and its wording, shared by the two fold sites — the authoring run (loop-tick.ts) and
|
|
16
|
+
* this module's follow-up turns — so they cannot drift. */
|
|
17
|
+
export async function foldAgentCommitsSince(ctx, wt, base) {
|
|
18
|
+
const folded = await foldCommitsSince(wt, base);
|
|
19
|
+
if (folded > 0)
|
|
20
|
+
ctx.warn(`agent made ${plural(folded, "commit")} during the run; folded into tumwater's commit`);
|
|
21
|
+
}
|
|
22
|
+
/** Stage a changed worktree for the merge queue: derive the commit message from the reply's
|
|
13
23
|
* SUMMARY block (one bounded follow-up turn when the run left none), flag high-friction ticks,
|
|
14
|
-
* commit, pin the commit by the role's
|
|
24
|
+
* commit, pin the commit by the role's merge ref, and enqueue the merge. Extracted from
|
|
15
25
|
* LoopRunner.runTick so the tick's own method stays the outcome classifier and this module is
|
|
16
|
-
* the commit-staging layer — the only place a fresh tick's work becomes a queued
|
|
26
|
+
* the commit-staging layer — the only place a fresh tick's work becomes a queued merge.
|
|
17
27
|
* Returns the tick's outcome; a failed pin or an aborted follow-up returns an error/aborted
|
|
18
28
|
* outcome without anything queued. */
|
|
19
|
-
export async function
|
|
29
|
+
export async function stageTickMerge(ctx) {
|
|
20
30
|
const s = ctx.state;
|
|
21
31
|
// A revision tick carries the round it applied on its context (loop-tick.ts): it rides onto the
|
|
22
|
-
// queued
|
|
32
|
+
// queued merge so a rejection of this change records the NEXT round (plans/revise-rejected.md).
|
|
23
33
|
// Undefined for a fresh change — including a user-request tick that leaves a pending revision
|
|
24
34
|
// untouched — so only the tick that actually held the rejected diff is labeled a revision.
|
|
25
35
|
const revisionRound = ctx.revisionRound;
|
|
26
|
-
// A revision
|
|
27
|
-
// part 2/2): the prior head
|
|
28
|
-
// can check each one against the interdiff
|
|
29
|
-
//
|
|
30
|
-
//
|
|
36
|
+
// A revision merge carries the review that rejected the change (plans/revise-rejected.md
|
|
37
|
+
// part 2/2): the prior head, its numbered objections, and each objection's persistence count
|
|
38
|
+
// ride the queued entry so the re-review can check each one against the interdiff and the next
|
|
39
|
+
// rejection can resolve a `(prior N)` restatement (BUGS.md 2026-10-14). Only a tick that
|
|
40
|
+
// actually held the rejected diff attaches it; a fresh change — including a user-request tick
|
|
41
|
+
// that leaves a pending revision untouched — and a recovery merge carry none.
|
|
31
42
|
const priorReview = revisionRound !== undefined && s.lastReview?.head
|
|
32
|
-
? {
|
|
43
|
+
? {
|
|
44
|
+
sha: s.lastReview.head,
|
|
45
|
+
reasons: s.lastReview.reasons,
|
|
46
|
+
...(s.revision?.reasonCounts?.length ? { counts: s.revision.reasonCounts } : {}),
|
|
47
|
+
}
|
|
33
48
|
: undefined;
|
|
34
49
|
// The commit subject and body come from the reply's closing block. A run that changed files
|
|
35
50
|
// without one — a cut-off final message, or plain non-compliance — gets one bounded follow-up
|
|
@@ -38,7 +53,9 @@ export async function stageTickLanding(ctx) {
|
|
|
38
53
|
let summary = extractSummary(ctx.finalText);
|
|
39
54
|
let body = extractCommitBody(ctx.finalText);
|
|
40
55
|
if (summary === null) {
|
|
56
|
+
const base = await headOf(ctx.wt, "HEAD");
|
|
41
57
|
const followUp = await ctx.requestSummary(ctx.wt);
|
|
58
|
+
await foldAgentCommitsSince(ctx, ctx.wt, base);
|
|
42
59
|
if (followUp?.aborted)
|
|
43
60
|
return ctx.finishAbortedTick();
|
|
44
61
|
if (followUp) {
|
|
@@ -46,7 +63,7 @@ export async function stageTickLanding(ctx) {
|
|
|
46
63
|
body = body ?? extractCommitBody(followUp.finalText);
|
|
47
64
|
}
|
|
48
65
|
if (summary === null)
|
|
49
|
-
summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, s.
|
|
66
|
+
summary = fallbackSummary(await changedFiles(ctx.wt), ctx.role, s.runs);
|
|
50
67
|
ctx.warn(`reply had no SUMMARY line — ` +
|
|
51
68
|
(followUp && extractSummary(followUp.finalText) !== null
|
|
52
69
|
? "recovered it with a follow-up turn"
|
|
@@ -57,15 +74,34 @@ export async function stageTickLanding(ctx) {
|
|
|
57
74
|
? `follow-up gave none; subject derived from the changed files: "${summary}"`
|
|
58
75
|
: `no follow-up session was available; subject derived from the changed files: "${summary}"`));
|
|
59
76
|
}
|
|
77
|
+
else {
|
|
78
|
+
// A SUMMARY the cap would cut mid-word gets one follow-up turn asking for it restated short.
|
|
79
|
+
const over = summaryOverCap(ctx.finalText);
|
|
80
|
+
if (over !== null) {
|
|
81
|
+
const base = await headOf(ctx.wt, "HEAD");
|
|
82
|
+
const followUp = await ctx.requestSummary(ctx.wt, over);
|
|
83
|
+
await foldAgentCommitsSince(ctx, ctx.wt, base);
|
|
84
|
+
if (followUp?.aborted)
|
|
85
|
+
return ctx.finishAbortedTick();
|
|
86
|
+
const short = followUp && summaryOverCap(followUp.finalText) === null ? extractSummary(followUp.finalText) : null;
|
|
87
|
+
if (short !== null) {
|
|
88
|
+
summary = short;
|
|
89
|
+
body = body ?? extractCommitBody(followUp.finalText);
|
|
90
|
+
}
|
|
91
|
+
ctx.warn(short !== null
|
|
92
|
+
? `SUMMARY was ${over} characters — shortened with a follow-up turn`
|
|
93
|
+
: `SUMMARY was ${over} characters — follow-up gave no shorter one; subject truncated`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
60
96
|
// The gate's deterministic backlog checks run here, before the commit, while the authoring
|
|
61
|
-
// session can still be continued: a finding that reaches review costs a queue slot,
|
|
62
|
-
// and a rejection that discards the work. One bounded follow-up turn on the
|
|
63
|
-
// session gets to fix them; the gate still has the final say, and the change commits
|
|
64
|
-
// queues either way.
|
|
97
|
+
// session can still be continued: a finding that reaches review costs a queue slot,
|
|
98
|
+
// a verification, and a rejection that discards the work. One bounded follow-up turn on the
|
|
99
|
+
// author's own session gets to fix them; the gate still has the final say, and the change commits
|
|
100
|
+
// and queues either way.
|
|
65
101
|
// Claims made at staging (plans/parallel-work-instances.md "Claims", part 4/7): an
|
|
66
102
|
// unassigned tick of a MULTI-instance base whose diff moved a backlog entry out of its
|
|
67
103
|
// actionable section records the claim so a sibling is not assigned it while this change
|
|
68
|
-
//
|
|
104
|
+
// merges. A single-runner base has no sibling to hold the entry from, so it stages nothing
|
|
69
105
|
// and is unchanged by claims. An assigned tick that moved a DIFFERENT entry gets a finding,
|
|
70
106
|
// so the one fix-up turn can put it back. So does an unassigned tick that moved an entry a
|
|
71
107
|
// sibling holds (the poll's heldElsewhere, which its prompt listed as off-limits): that entry
|
|
@@ -83,14 +119,14 @@ export async function stageTickLanding(ctx) {
|
|
|
83
119
|
const first = claimable[0];
|
|
84
120
|
s.claim = { file: first.file, key: first.key, title: first.title, at: Date.now(), source: "staged" };
|
|
85
121
|
// The assignment is durable in state before this event; an unwritable feed must not reject
|
|
86
|
-
// the tick and make a claim that did
|
|
122
|
+
// the tick and make a claim that did take effect look unapplied (logEventBestEffort).
|
|
87
123
|
logEventBestEffort(ctx.root, {
|
|
88
|
-
|
|
124
|
+
source: ctx.role,
|
|
89
125
|
type: "claim",
|
|
90
126
|
action: "assigned",
|
|
91
127
|
key: first.key,
|
|
92
128
|
title: first.title,
|
|
93
|
-
|
|
129
|
+
origin: "staged",
|
|
94
130
|
});
|
|
95
131
|
}
|
|
96
132
|
const assignedMoved = s.claim ? assignedMovedFinding(s.claim.key, moved) : undefined;
|
|
@@ -101,7 +137,9 @@ export async function stageTickLanding(ctx) {
|
|
|
101
137
|
findings.push(heldMoved);
|
|
102
138
|
if (findings.length > 0) {
|
|
103
139
|
const before = findings.length;
|
|
140
|
+
const fixBase = await headOf(ctx.wt, "HEAD");
|
|
104
141
|
const fixUp = await ctx.requestStageFix(ctx.wt, findings);
|
|
142
|
+
await foldAgentCommitsSince(ctx, ctx.wt, fixBase);
|
|
105
143
|
if (fixUp?.aborted)
|
|
106
144
|
return ctx.finishAbortedTick();
|
|
107
145
|
if (fixUp) {
|
|
@@ -137,53 +175,59 @@ export async function stageTickLanding(ctx) {
|
|
|
137
175
|
const authoringTurns = ctx.tickTurns;
|
|
138
176
|
const highFriction = authoringTurns > ctx.config.thrashTurns && minutes > ctx.config.thrashMinutes;
|
|
139
177
|
if (highFriction) {
|
|
140
|
-
ctx.warn(`high-friction
|
|
178
|
+
ctx.warn(`high-friction run: ${authoringTurns} turns in ${Math.round(minutes)} min ` +
|
|
141
179
|
`(thresholds: ${ctx.config.thrashTurns} turns / ${ctx.config.thrashMinutes} min)`);
|
|
142
180
|
}
|
|
143
181
|
// The trailer is harness-stamped truth: turns and peak ctx over this tick's pre-commit
|
|
144
182
|
// runs only (conflict-resolution and review runs fold after the commit). A high-friction
|
|
145
183
|
// tick appends its Friction line here — both values are already computed above.
|
|
146
|
-
const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), summary), body, commitTrailer(ctx.role, s.
|
|
184
|
+
const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), summary), body, commitTrailer(ctx.role, s.runs, authoringTurns, s.peakContextTokens, highFriction ? minutes : undefined, revisionRound));
|
|
185
|
+
// The backlog archive (plans/backlog-archive.md): an entry this change pasted under PLANS.md's
|
|
186
|
+
// ## Done or BUGS.md's ## Fixed moves to the top of PLANS-DONE.md or BUGS-FIXED.md now, after
|
|
187
|
+
// the self-check and its fix-up turn worked on the tree the author wrote and before the
|
|
188
|
+
// commit, so the review and the merge see the archived form and the live files keep only
|
|
189
|
+
// open work. A failed sweep leaves the entry where the author put it; nothing reads it lost.
|
|
190
|
+
if (ctx.mainBranch)
|
|
191
|
+
await archiveNewFinishedEntries(ctx.wt, ctx.mainBranch).catch(() => []);
|
|
147
192
|
const commit = await commitAll(ctx.wt, message);
|
|
148
|
-
// Pin the commit by its
|
|
149
|
-
// to the
|
|
150
|
-
// slot, never in this worktree (plans/merge-queue.md 2/5). A failed pin defers to
|
|
151
|
-
// recovery —
|
|
152
|
-
// The reviewer checks the author's claimed WHY/VERIFIED against the actual diff; no diff
|
|
193
|
+
// Pin the commit by its merge ref BEFORE freeing the worktree (invariant 4), then hand it
|
|
194
|
+
// to the merge queue: from here on the review gate and the rebase run in a verification's
|
|
195
|
+
// leased pool slot, never in this worktree (plans/merge-queue.md 2/5). A failed pin defers to
|
|
196
|
+
// next-tick recovery — merge without a pin would lose the ref lifecycle this whole flow depends
|
|
197
|
+
// on. The reviewer checks the author's claimed WHY/VERIFIED against the actual diff; no diff
|
|
153
198
|
// reaches main unreviewed.
|
|
154
199
|
if (!(await ctx.pinAndReset(ctx.wt, commit))) {
|
|
155
|
-
s.lastError = "failed to pin the
|
|
200
|
+
s.lastError = "failed to pin the merge ref; left for the next run's recovery";
|
|
156
201
|
return { result: "error", summary: s.lastError };
|
|
157
202
|
}
|
|
158
|
-
// The commit is pinned and the worktree is free: enqueue the
|
|
159
|
-
// orchestrator drains the queue on its single
|
|
160
|
-
// this slot is free the moment the work is committed (plans/merge-queue.md 3/5). The
|
|
161
|
-
// runs the same gate +
|
|
203
|
+
// The commit is pinned and the worktree is free: enqueue the merge and END the tick — the
|
|
204
|
+
// orchestrator drains the queue on its single merge slot, outside the author semaphore, so
|
|
205
|
+
// this slot is free the moment the work is committed (plans/merge-queue.md 3/5). The merge
|
|
206
|
+
// runs the same gate + merge flow through runMergePi/foldMergeUsage on this same state
|
|
162
207
|
// object — recording the commit count, the outcome, and the reviewer spend into it — and logs
|
|
163
|
-
//
|
|
164
|
-
// `commits` was NOT incremented above: it counts
|
|
165
|
-
|
|
208
|
+
// merge_done/merge_failed; a non-terminal outcome keeps the pin for next-tick leftover recovery.
|
|
209
|
+
// `commits` was NOT incremented above: it counts merged changes only.
|
|
210
|
+
enqueueMerge(ctx.root, {
|
|
166
211
|
role: ctx.role,
|
|
167
212
|
sha: commit,
|
|
168
|
-
|
|
213
|
+
run: s.runs,
|
|
169
214
|
summary,
|
|
170
215
|
body: body ? formatCommitBody(body) : undefined,
|
|
171
216
|
highFriction: highFriction || undefined,
|
|
172
|
-
...(
|
|
173
|
-
...(priorReview !== undefined ? { priorReview } : {}),
|
|
217
|
+
...entryRevisionFields({ revisionRound, priorReview }),
|
|
174
218
|
enqueuedAt: Date.now(),
|
|
175
219
|
});
|
|
176
|
-
// The revision is now in flight: the rejected ref's lifecycle continues in the
|
|
177
|
-
// (
|
|
220
|
+
// The revision is now in flight: the rejected ref's lifecycle continues in the merger
|
|
221
|
+
// (merge-core.ts), which deletes it when this merge merges and repoints it on a rejection.
|
|
178
222
|
// The round is also stamped on the commit's Revision trailer above, so leftover recovery can
|
|
179
|
-
// rebuild a retriable revision's
|
|
223
|
+
// rebuild a retriable revision's merge entry with its round when the queue marker is lost.
|
|
180
224
|
if (revisionRound !== undefined)
|
|
181
225
|
s.revision = undefined;
|
|
182
|
-
// The
|
|
226
|
+
// The merge sits in the durable queue above and the commit is pinned; the event is
|
|
183
227
|
// observability, so an unwritable feed must not reject a tick whose work is already queued.
|
|
184
|
-
logEventBestEffort(ctx.root, {
|
|
228
|
+
logEventBestEffort(ctx.root, { source: ctx.role, type: "merge_queued", commit, summary });
|
|
185
229
|
// The flag's durable record is the Friction trailer line stamped on the commit above;
|
|
186
|
-
// lastSummary and the
|
|
230
|
+
// lastSummary and the run_end event carry it too for dashboards and logs.
|
|
187
231
|
const finalSummary = highFriction
|
|
188
232
|
? `${summary} (high friction: ${authoringTurns} turns / ${Math.round(minutes)}m)`
|
|
189
233
|
: summary;
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
|
|
2
|
-
import { warnEvent } from "../events/events.js";
|
|
3
|
+
import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
|
|
3
4
|
import { removeQuiet } from "../files/files.js";
|
|
4
|
-
import {
|
|
5
|
+
import { mergeStatePath } from "../paths.js";
|
|
5
6
|
import { runOnAbort } from "../process/process.js";
|
|
6
7
|
/** The orchestrator's tick-timing and scheduling seams (src/orchestrator/orchestrator.ts
|
|
7
8
|
* keeps the poll loop itself — the control flow that calls these). Each is exported as a
|
|
8
9
|
* unit-test seam (test/orchestrator-seams.test.ts): timing a semaphore-gated tick, an
|
|
9
|
-
* abort-interruptible sleep, and the restart hand-off's bounded wait on the in-flight
|
|
10
|
+
* abort-interruptible sleep, and the restart hand-off's bounded wait on the in-flight merge.
|
|
10
11
|
* Split out of orchestrator.ts — which had grown into both the poll loop and the timing helpers
|
|
11
|
-
* it schedules with — so the loop reads as control flow over these named steps. The
|
|
12
|
-
* polls (the
|
|
13
|
-
* in src/
|
|
12
|
+
* it schedules with — so the loop reads as control flow over these named steps. The team-health
|
|
13
|
+
* polls (the team hold, the error-storm warning, the failure-spread alarm) are their own family
|
|
14
|
+
* in src/team/team-polls.ts. */
|
|
14
15
|
/** How many completed role-tick samples the p75 needs before it is trusted as the drain window.
|
|
15
16
|
* Below this the orchestrator reports no p75 and poll keeps the cold-start constant. */
|
|
16
17
|
const DRAIN_P75_MIN_SAMPLES = 10;
|
|
@@ -25,39 +26,39 @@ export function p75TickDurationMs(durations) {
|
|
|
25
26
|
const sorted = [...durations].sort((a, b) => a - b);
|
|
26
27
|
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))] ?? null;
|
|
27
28
|
}
|
|
28
|
-
/** How long a restart's hand-off waits on an in-flight
|
|
29
|
+
/** How long a restart's hand-off waits on an in-flight merge, per phase: one window for it to
|
|
29
30
|
* finish on its own, then — having aborted it — one more for the step it was in to end
|
|
30
|
-
* (
|
|
31
|
-
* build is already in dist/ and every role tick is done or aborted, so the
|
|
32
|
-
* is still running: each minute spent on it is a minute nothing else ticks. A
|
|
31
|
+
* (awaitMergeForHandoff; BUGS.md 2026-09-23). By the time a restart reaches its `finally` the new
|
|
32
|
+
* build is already in dist/ and every role tick is done or aborted, so the merge is all the team
|
|
33
|
+
* is still running: each minute spent on it is a minute nothing else ticks. A merge's unbounded
|
|
33
34
|
* part is model work — its reviewer run (and, until 2026-09-24, a gate build-fix run: the
|
|
34
35
|
* 2026-09-23 one held the slot for 4 h 35 m and that day's hand-off sat through 97 minutes of it).
|
|
35
36
|
* Its deterministic part is bounded: at most one build check (BUILD_CHECK_TIMEOUT_MS) plus git
|
|
36
|
-
* steps. So a check's bound plus a minute lets a
|
|
37
|
-
* last check and fast-forward —
|
|
38
|
-
* no abort reaches) time to end, so the hand-off does not leave a check running in a
|
|
37
|
+
* steps. So a check's bound plus a minute lets a merge already past its model runs — in its
|
|
38
|
+
* last check and fast-forward — merge, and gives an aborted merge's current step (a check, which
|
|
39
|
+
* no abort reaches) time to end, so the hand-off does not leave a check running in a merger
|
|
39
40
|
* worktree the next generation is about to reset. Not the drain's window: that is the p75 of whole
|
|
40
|
-
* role ticks (over an hour on this
|
|
41
|
-
export const
|
|
41
|
+
* role ticks (over an hour on this team) — the very lag this bounds. */
|
|
42
|
+
export const HANDOFF_MERGE_WINDOW_MS = BUILD_CHECK_TIMEOUT_MS + 60_000;
|
|
42
43
|
/** Run one role tick under the concurrency semaphore and return how long the tick itself ran,
|
|
43
44
|
* in ms — null when it never started (the harness is already stopping, or `held`) or was cut
|
|
44
45
|
* off by an abort. This is the restart drain's p75 sample (BUGS.md 2026-09-18). The clock starts
|
|
45
46
|
* only once the permit is granted, so time a tick spends parked in the semaphore queue is not
|
|
46
47
|
* counted as work: the drain waits on ticks that are already running, and folding queue wait
|
|
47
|
-
* into the window would overstate how long they have left (the `
|
|
48
|
+
* into the window would overstate how long they have left (the `run_start`..`run_end` span
|
|
48
49
|
* the window was sized against excludes it too). Aborted ticks return null so their short
|
|
49
50
|
* cut-off lengths cannot drag the window down. So do ticks that ended with nothing to show
|
|
50
51
|
* (`no_change`, `skipped`): a `main moved` re-check runs the model and still finishes in
|
|
51
|
-
* seconds, and sampling it equally let a
|
|
52
|
+
* seconds, and sampling it equally let a team's window collapse to the re-check scale and
|
|
52
53
|
* cut real work off minutes into its run (BUGS.md 2026-09-30) — only a work-bearing outcome
|
|
53
54
|
* says how long the work the drain waits on takes. `now` is a test seam.
|
|
54
55
|
*
|
|
55
|
-
* `held` is the
|
|
56
|
+
* `held` is the team-wide hold on new ticks (today the restart drain's), re-checked at the one
|
|
56
57
|
* moment a reserved tick actually begins: when its permit is granted. A tick scheduled before
|
|
57
58
|
* the hold may have parked in the semaphore queue long before it; checking the hold only at
|
|
58
59
|
* scheduling let every such waiter start a fresh pi run mid-drain as slots freed — the very
|
|
59
60
|
* ticks the restart then aborted (BUGS.md 2026-09-23). A held tick releases its permit without
|
|
60
|
-
* calling `tick`, so it writes no state and logs no
|
|
61
|
+
* calling `tick`, so it writes no state and logs no run_start/run_end; the caller hands its
|
|
61
62
|
* reservation back. */
|
|
62
63
|
export async function runTimedRoleTick(signal, acquire, release, tick, now = Date.now, held = () => false) {
|
|
63
64
|
await acquire();
|
|
@@ -118,78 +119,78 @@ function settlesWithin(promise, ms, after) {
|
|
|
118
119
|
promise.then(() => done(true), () => done(true));
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
|
-
/** The restart hand-off's bounded wait on the in-flight
|
|
122
|
-
* `finally`'s
|
|
123
|
-
*
|
|
124
|
-
* unbounded await here is a
|
|
122
|
+
/** The restart hand-off's bounded wait on the in-flight merge (BUGS.md 2026-09-23): the shutdown
|
|
123
|
+
* `finally`'s merge await when this process is about to exit for the next generation. The
|
|
124
|
+
* merges run a reviewer and a build check per change and a merge's check after them, so an
|
|
125
|
+
* unbounded await here is a team-wide drain in disguise — the 2026-09-23 hand-off lagged its own
|
|
125
126
|
* swap by 97 minutes, in silence. The wait is announced as it starts (one warning naming the
|
|
126
|
-
*
|
|
127
|
-
* `orchestrator stopped`. Past `windowMs` the
|
|
127
|
+
* merge's roles), so the feed says why `restarting onto build …` is not yet followed by
|
|
128
|
+
* `orchestrator stopped`. Past `windowMs` the merge is stopped the way the drain gives up on role
|
|
128
129
|
* ticks: `abort` fires the harness's internal stop, which kills its pi runs at once and makes it
|
|
129
|
-
* stop at its next step boundary (
|
|
130
|
-
* signal before every gate, every approved
|
|
131
|
-
* warning names what was still awaited. An aborted
|
|
130
|
+
* stop at its next step boundary (merge-core.ts, merge-batch.ts, and merge-stack.ts check the
|
|
131
|
+
* signal before every gate, every approved merge, and each of a stack's check attempts), and a
|
|
132
|
+
* warning names what was still awaited. An aborted merge records `aborted` like any shutdown
|
|
132
133
|
* abort — pins kept, entries dropped, marker removed — for its roles' leftover recovery on the
|
|
133
134
|
* new build. One still running a window after the abort (a step wedged past its own bound) is left
|
|
134
135
|
* behind: its 4/5 marker is cleared here, since this process exits the moment the hand-off returns,
|
|
135
136
|
* and its entries and pins survive exactly as a crash leaves them — the next generation's first
|
|
136
|
-
* drain re-
|
|
137
|
-
export async function
|
|
138
|
-
const roles =
|
|
137
|
+
* drain re-merges them. Exported as a unit-test seam, like runTimedRoleTick. */
|
|
138
|
+
export async function awaitMergeForHandoff(root, merge, windowMs, abort, after = realAfterDelay) {
|
|
139
|
+
const roles = merge.roles.join(", ");
|
|
139
140
|
const deadline = `${windowMs / 1000}s`;
|
|
140
|
-
warnEvent(root,
|
|
141
|
-
if (await settlesWithin(
|
|
141
|
+
warnEvent(root, TUMWATER_SOURCE, `restart waiting on the merge in progress for ${roles} (deadline ${deadline})`);
|
|
142
|
+
if (await settlesWithin(merge.promise, windowMs, after))
|
|
142
143
|
return "finished";
|
|
143
144
|
abort();
|
|
144
|
-
warnEvent(root,
|
|
145
|
-
if (await settlesWithin(
|
|
145
|
+
warnEvent(root, TUMWATER_SOURCE, `restart: the merge for ${roles} outlived its ${deadline} deadline — aborted; its pinned commits survive for the next restart`);
|
|
146
|
+
if (await settlesWithin(merge.promise, windowMs, after))
|
|
146
147
|
return "aborted";
|
|
147
|
-
removeQuiet(
|
|
148
|
-
warnEvent(root,
|
|
148
|
+
removeQuiet(mergeStatePath(root));
|
|
149
|
+
warnEvent(root, TUMWATER_SOURCE, `restart: the aborted merge for ${roles} was still running ${deadline} later — restarting without it; its queue entries and pinned commits survive for the next restart`);
|
|
149
150
|
return "abandoned";
|
|
150
151
|
}
|
|
151
152
|
/**
|
|
152
153
|
* Drain the orchestrator's in-flight work at shutdown: wait out the reserved role and director
|
|
153
|
-
* ticks, and await the
|
|
154
|
-
* next. On an operator stop the harness signal has already aborted the
|
|
154
|
+
* ticks, and await the merge pipeline's tasks beside them — and how depends on what comes
|
|
155
|
+
* next. On an operator stop the harness signal has already aborted the merge — its pi runs die
|
|
155
156
|
* and it stops at its next step boundary, ending "aborted" with its ref kept and its entry
|
|
156
157
|
* dropped for next-start recovery — and it is waited out: returning early would remove
|
|
157
|
-
* orchestrator.json while the process still
|
|
158
|
-
* concurrent
|
|
159
|
-
* the next generation the moment this returns, and a
|
|
160
|
-
* — a merge's check follows the
|
|
161
|
-
* (
|
|
162
|
-
* each
|
|
163
|
-
* aborted) together under the roles of them all. The
|
|
164
|
-
* no task, and a
|
|
165
|
-
* once; a restart's closed start gate at its grant): all of them keep their
|
|
166
|
-
* for the next start.
|
|
158
|
+
* orchestrator.json while the process is still merging, letting a second `tumwater run` start a
|
|
159
|
+
* concurrent merger (a second Ctrl+C still forces the exit). On a restart the caller exits for
|
|
160
|
+
* the next generation the moment this returns, and a merge is not bounded by one reviewer run
|
|
161
|
+
* — a merge's check follows the verifications' — so the wait is a bounded hand-off that
|
|
162
|
+
* announces itself (awaitMergeForHandoff; BUGS.md 2026-09-23). "The merge" is every merge task
|
|
163
|
+
* at once — each verification holding a permit, and the merge — waited on (and, past the hand-off's
|
|
164
|
+
* window, aborted) together under the roles of them all. The approved changes waiting for the
|
|
165
|
+
* merge have no task, and a verification still parked for its permit has started nothing (a
|
|
166
|
+
* shutdown settles it at once; a restart's closed start gate at its grant): all of them keep their
|
|
167
|
+
* entries and pins for the next start.
|
|
167
168
|
*/
|
|
168
169
|
export async function drainInFlightWork(root, roleInFlight, directorInFlight, inFlight, restart, handoffWindowMs, abort, after = realAfterDelay) {
|
|
169
170
|
const ticks = Promise.allSettled([...roleInFlight, ...directorInFlight]);
|
|
170
|
-
const
|
|
171
|
+
const merge = inFlight.length === 0
|
|
171
172
|
? null
|
|
172
173
|
: {
|
|
173
174
|
promise: Promise.allSettled(inFlight.map((t) => t.promise)).then(() => { }),
|
|
174
175
|
roles: inFlight.flatMap((t) => t.roles),
|
|
175
176
|
};
|
|
176
|
-
if (
|
|
177
|
-
// The abort is the caller's internal stop, not just the
|
|
177
|
+
if (merge && restart) {
|
|
178
|
+
// The abort is the caller's internal stop, not just the merge's own controller: each task
|
|
178
179
|
// wires the harness signal to its controller, and a conflict-resolution run watches the
|
|
179
|
-
// harness signal alone (
|
|
180
|
+
// harness signal alone (runMergePi). Nothing else it reaches can start work here — the
|
|
180
181
|
// director has finished, permit holders were aborted at the restart, and a parked waiter
|
|
181
182
|
// meets the closed start gate (tickStartHeld) whenever it is granted a permit.
|
|
182
|
-
const outcome = await
|
|
183
|
-
// An abandoned
|
|
183
|
+
const outcome = await awaitMergeForHandoff(root, merge, handoffWindowMs, abort, after);
|
|
184
|
+
// An abandoned merge still holds its permit, so a waiter parked behind it would never be
|
|
184
185
|
// granted one and never settle. The ticks have had both windows beside the hand-off, so
|
|
185
|
-
// whatever is still reserved then started nothing (or is wedged like the
|
|
186
|
+
// whatever is still reserved then started nothing (or is wedged like the merge): the
|
|
186
187
|
// hand-off goes ahead without it too.
|
|
187
188
|
if (outcome !== "abandoned")
|
|
188
189
|
await ticks;
|
|
189
190
|
}
|
|
190
191
|
else {
|
|
191
|
-
if (
|
|
192
|
-
warnEvent(root,
|
|
193
|
-
await Promise.allSettled([ticks,
|
|
192
|
+
if (merge)
|
|
193
|
+
warnEvent(root, TUMWATER_SOURCE, `shutdown waiting on the merge in progress for ${merge.roles.join(", ")}`);
|
|
194
|
+
await Promise.allSettled([ticks, merge?.promise]);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
@@ -3,12 +3,12 @@ import { recordDailyCost } from "../budget/budget.js";
|
|
|
3
3
|
import { nonNegativeNumber } from "../files/json-object.js";
|
|
4
4
|
/** Usage accounting for one role loop, split out of loop.ts — which keeps the tick lifecycle —
|
|
5
5
|
* because token/cost/turn bookkeeping is a self-contained concern with its own consumers: the
|
|
6
|
-
* commit trailer reads turns, the
|
|
7
|
-
* hold reads lastRateLimit and lastBackendFailure (src/
|
|
8
|
-
* src/
|
|
6
|
+
* commit trailer reads turns, the run_end event reads costUsd, and the orchestrator's team-wide
|
|
7
|
+
* hold reads lastRateLimit and lastBackendFailure (src/team/team-hold.ts,
|
|
8
|
+
* src/team/team-polls.ts).
|
|
9
9
|
*
|
|
10
10
|
* fold() is the once-per-run choke point: every pi run of a tick — main attempt, transient
|
|
11
|
-
* retry, conflict resolution,
|
|
11
|
+
* retry, conflict resolution, merge runs via foldMergeUsage — ends up here exactly once, so
|
|
12
12
|
* adding a usage field to PiRunResult touches this single place. */
|
|
13
13
|
/** One persisted counter, healed to a finite non-negative number before it is read and
|
|
14
14
|
* rewritten: `generatedTokens`/`peakContextTokens` are per-tick windows and `totalCostUsd` a
|
|
@@ -32,12 +32,12 @@ export class TickUsage {
|
|
|
32
32
|
turns = 0;
|
|
33
33
|
/** USD cost folded into THIS tick so far (non-persisted): reset at tick start alongside
|
|
34
34
|
* turns, grown in fold. Deliberately not on LoopState — unlike generatedTokens, no
|
|
35
|
-
* dashboard reads it mid-run; its only consumer is the
|
|
35
|
+
* dashboard reads it mid-run; its only consumer is the run_end event, which fires before
|
|
36
36
|
* the next tick resets it (plans: per-tick usage in the event feed). */
|
|
37
37
|
costUsd = 0;
|
|
38
38
|
/** Subscription runs' cost folded into THIS tick (non-persisted): API-equivalent spend the
|
|
39
39
|
* run reported but that never charges the daily cap (plans/agent-backends.md part 6a). The
|
|
40
|
-
*
|
|
40
|
+
* run_end event carries it and the report shows it on its own notional line. Reset at tick
|
|
41
41
|
* start like costUsd. */
|
|
42
42
|
notionalCostUsd = 0;
|
|
43
43
|
/** How the tick's runs were last reported as paid for, and the newest subscription
|
|
@@ -48,31 +48,38 @@ export class TickUsage {
|
|
|
48
48
|
rateWindows;
|
|
49
49
|
/** Prompt tokens folded into THIS tick so far (non-persisted, reset at tick start). Every
|
|
50
50
|
* pi run of the tick contributes — the authoring run, its transient retry, the SUMMARY
|
|
51
|
-
* follow-up, and (through
|
|
51
|
+
* follow-up, and (through foldMergeUsage) review and conflict-resolution runs — because
|
|
52
52
|
* all of them re-send context the operator paid for. */
|
|
53
53
|
promptTokens = 0;
|
|
54
54
|
/** The cache-read share of promptTokens folded so far. */
|
|
55
55
|
cacheReadTokens = 0;
|
|
56
56
|
/** Prompt tokens folded before the tick's first edit: authoring runs only, frozen once an
|
|
57
|
-
* authoring run reports an edit (plans/per-tick-prompt-telemetry).
|
|
57
|
+
* authoring run reports an edit (plans/per-tick-prompt-telemetry). Merge, review and
|
|
58
58
|
* conflict-resolution runs are excluded — their context is the gate's, not the authoring
|
|
59
59
|
* tick's pre-edit search. */
|
|
60
60
|
preEditPromptTokens = 0;
|
|
61
61
|
/** True once an authoring run has reported a first edit; the pre-edit counter freezes. */
|
|
62
62
|
editSeen = false;
|
|
63
63
|
/** This loop's most recent pi run that ended on a provider 429 — author run, retry,
|
|
64
|
-
* reviewer or
|
|
64
|
+
* reviewer or merge run alike, since every one folds through fold — with the provider's
|
|
65
65
|
* Retry-After hint when it sent one. The orchestrator reads it every poll as this role's
|
|
66
|
-
* rate-limit input to the
|
|
66
|
+
* rate-limit input to the team-wide hold (src/team/team-hold.ts): the per-run retry has no
|
|
67
67
|
* cross-role view, and a field on the runner the orchestrator already holds carries the
|
|
68
68
|
* fact without a new cross-module channel. In memory only — a hold is minutes long, so a
|
|
69
69
|
* restart forgetting it costs nothing. Undefined until the first such run. */
|
|
70
70
|
lastRateLimit;
|
|
71
|
+
/** Per-provider spend folded into THIS tick (non-persisted, reset at tick start): each
|
|
72
|
+
* run's real/notional cost keyed by the provider it ran on, so the run_end event can say
|
|
73
|
+
* which seam of a mixed team spent what (plans: per-provider spend in the day report). A
|
|
74
|
+
* run whose provider was never resolved attributes nothing — the `provider !== undefined`
|
|
75
|
+
* guard below skips the bump — so an old single-provider tick keeps today's byte-identical
|
|
76
|
+
* behavior. Parallel to costUsd/notionalCostUsd, never subtracted from them. */
|
|
77
|
+
providerCost = new Map();
|
|
71
78
|
/** This loop's most recent pi run that ended on a provider-wide failure that was NOT rate
|
|
72
79
|
* limiting — the connection down, a 5xx, the model failing to load (PiRunResult.
|
|
73
80
|
* transientBackend) — with the kind the hold's storm test groups storms by. Stamped under
|
|
74
|
-
* the same rule and for the same consumer as lastRateLimit above: the
|
|
75
|
-
* (src/
|
|
81
|
+
* the same rule and for the same consumer as lastRateLimit above: the team-wide hold
|
|
82
|
+
* (src/team/team-hold.ts) reads it every poll. In memory only, like lastRateLimit.
|
|
76
83
|
* Undefined until the first such run. */
|
|
77
84
|
lastBackendFailure;
|
|
78
85
|
/** Clear the per-tick windows (turns, costUsd, the prompt-token counters) at tick start.
|
|
@@ -88,12 +95,13 @@ export class TickUsage {
|
|
|
88
95
|
this.cacheReadTokens = 0;
|
|
89
96
|
this.preEditPromptTokens = 0;
|
|
90
97
|
this.editSeen = false;
|
|
98
|
+
this.providerCost = new Map();
|
|
91
99
|
}
|
|
92
100
|
/** Fold one pi run's usage into the tick's counters (gen / peak ctx / cost / turns) and
|
|
93
|
-
* the loop's lifetime totals on `s`. `authoring` is false for a
|
|
94
|
-
* through
|
|
101
|
+
* the loop's lifetime totals on `s`. `authoring` is false for a merge/review run folded
|
|
102
|
+
* through foldMergeUsage: those never advance the tick's pre-edit counter, which measures
|
|
95
103
|
* the authoring search. */
|
|
96
|
-
fold(s, run, authoring = true) {
|
|
104
|
+
fold(s, run, authoring = true, provider, pair) {
|
|
97
105
|
s.generatedTokens = counter(s.generatedTokens) + run.outputTokens;
|
|
98
106
|
s.peakContextTokens = Math.max(counter(s.peakContextTokens), run.peakContextTokens);
|
|
99
107
|
s.totalCostUsd = counter(s.totalCostUsd) + run.costUsd;
|
|
@@ -103,12 +111,14 @@ export class TickUsage {
|
|
|
103
111
|
if (runCostIsReal(run)) {
|
|
104
112
|
this.costUsd += run.costUsd;
|
|
105
113
|
// The daily cost budget window (plans/daily-cost-budget.md): every real-money pi run of
|
|
106
|
-
// a tick folds here exactly once, so the
|
|
114
|
+
// a tick folds here exactly once, so the team's spend for the local day is complete at
|
|
107
115
|
// each tick end.
|
|
108
|
-
recordDailyCost(s, run.costUsd);
|
|
116
|
+
recordDailyCost(s, run.costUsd, Date.now(), pair);
|
|
117
|
+
this.bumpProviderCost(provider, run.costUsd, 'costUsd');
|
|
109
118
|
}
|
|
110
119
|
else {
|
|
111
120
|
this.notionalCostUsd += run.costUsd;
|
|
121
|
+
this.bumpProviderCost(provider, run.costUsd, 'notionalCostUsd');
|
|
112
122
|
}
|
|
113
123
|
if (run.billing !== undefined)
|
|
114
124
|
this.billing = run.billing;
|
|
@@ -118,7 +128,7 @@ export class TickUsage {
|
|
|
118
128
|
this.cacheReadTokens += run.cacheReadTokens;
|
|
119
129
|
// The pre-edit prefix freezes at the tick's first edit: every authoring run while no edit
|
|
120
130
|
// has been seen adds its pre-edit count (including the run that edits), and once one
|
|
121
|
-
// reports an edit later runs add nothing. A review/
|
|
131
|
+
// reports an edit later runs add nothing. A review/merge run is skipped entirely.
|
|
122
132
|
if (authoring) {
|
|
123
133
|
if (!this.editSeen)
|
|
124
134
|
this.preEditPromptTokens += run.preEditPromptTokens;
|
|
@@ -126,17 +136,37 @@ export class TickUsage {
|
|
|
126
136
|
this.editSeen = true;
|
|
127
137
|
}
|
|
128
138
|
this.turns += run.turns;
|
|
129
|
-
// The
|
|
139
|
+
// The team-wide hold's rate-limit input (lastRateLimit above), from the same
|
|
130
140
|
// once-per-run choke point. Only a run that ENDED on the 429 counts: pi exits on the error,
|
|
131
141
|
// so "now" is when the provider refused — a run that merely logged one inside pi's own
|
|
132
142
|
// auto-retry and then
|
|
133
143
|
// finished would stamp a 429 at its end, possibly hours late, and could trip a false storm.
|
|
134
144
|
if (run.transientRateLimit && !run.ok)
|
|
135
|
-
this.lastRateLimit = { at: Date.now(), retryAfterSeconds: run.retryAfterSeconds };
|
|
145
|
+
this.lastRateLimit = { at: Date.now(), retryAfterSeconds: run.retryAfterSeconds, provider };
|
|
136
146
|
// The backend-failure sibling of the stamp above, same rule: only a run that ENDED on the
|
|
137
|
-
// failure counts, and the kind travels with it (src/pi/pi-
|
|
138
|
-
// the text at parse time). A 429 never
|
|
147
|
+
// failure counts, and the kind travels with it (src/pi/pi-failure.ts backendKind classified
|
|
148
|
+
// the text at parse time). A 429 never ends up here — pi-stream checks rate-limit first.
|
|
139
149
|
if (run.transientBackend && !run.ok && run.backendKind)
|
|
140
|
-
this.lastBackendFailure = { at: Date.now(), kind: run.backendKind };
|
|
150
|
+
this.lastBackendFailure = { at: Date.now(), kind: run.backendKind, provider };
|
|
151
|
+
}
|
|
152
|
+
/** Add `amount` to one field of the tick's per-provider spend ledger, initializing the
|
|
153
|
+
* provider's entry on first contact. `provider` undefined (a run that never resolved one)
|
|
154
|
+
* is a no-op, matching how fold calls it: only runs with a provider reach this. */
|
|
155
|
+
bumpProviderCost(provider, amount, field) {
|
|
156
|
+
if (provider === undefined)
|
|
157
|
+
return;
|
|
158
|
+
const e = this.providerCost.get(provider) ?? { costUsd: 0, notionalCostUsd: 0 };
|
|
159
|
+
e[field] += amount;
|
|
160
|
+
this.providerCost.set(provider, e);
|
|
161
|
+
}
|
|
162
|
+
/** Serialize the per-provider split for the run_end payload: a `Record<provider,
|
|
163
|
+
* {costUsd, notionalCostUsd}>`, or `{}` when no run carried a provider — the
|
|
164
|
+
* omit-when-empty idiom tick-finalize.ts applies, so a tick whose runs never resolved a
|
|
165
|
+
* provider renders no providerCost field at all. */
|
|
166
|
+
serializeProviderCost() {
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const [provider, e] of this.providerCost)
|
|
169
|
+
out[provider] = e;
|
|
170
|
+
return out;
|
|
141
171
|
}
|
|
142
172
|
}
|