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
|
@@ -3,10 +3,10 @@ import { DIRECTOR_ROLE, OBSERVER_ROLES } from "../roles/roles.js";
|
|
|
3
3
|
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
4
4
|
import { ERROR_BACKOFF, pushYieldOutcome, scheduleAtMinInterval, scheduleBackoff, } from "../scheduling/backoff.js";
|
|
5
5
|
import { firstReason } from "../text/phrases.js";
|
|
6
|
-
/** The state machine that applies a finished tick or
|
|
6
|
+
/** The state machine that applies a finished tick or merge to the loop's state: the
|
|
7
7
|
* outcome application (applyTickOutcome — which outcomes count which streaks, when a
|
|
8
|
-
* cut-off or quiet-killed session resumes and when it is abandoned), its
|
|
9
|
-
* (
|
|
8
|
+
* cut-off or quiet-killed session resumes and when it is abandoned), its merge sibling
|
|
9
|
+
* (applyMergeOutcome), the error-streak thresholds, the resume limits, and the
|
|
10
10
|
* review-verdict record the next tick's prompt reads. Split out of tick-outcome.ts — which
|
|
11
11
|
* keeps the TickResult/TickOutcome vocabulary the dashboards and call sites share — because
|
|
12
12
|
* "what an outcome is" and "what applying it records" are different concerns that happen to
|
|
@@ -19,13 +19,13 @@ export function recordReview(s, verdict, reasons, head) {
|
|
|
19
19
|
s.lastReview = { verdict, reasons, ...(head === undefined ? {} : { head }), at: Date.now() };
|
|
20
20
|
}
|
|
21
21
|
/** Drop a stale review rejection once the change it objected to is superseded (BUGS.md
|
|
22
|
-
* 2026-10-07): the review-exempt and refusal-note
|
|
22
|
+
* 2026-10-07): the review-exempt and refusal-note merges record no new verdict, so a reject
|
|
23
23
|
* would otherwise ride every later tick prompt as if it were the role's last review outcome.
|
|
24
|
-
* Called from the points a change
|
|
25
|
-
* `handleRefusal`'s note merge), never while a revision is still owed: a
|
|
24
|
+
* Called from the points a change merges (`applyMergeOutcome`'s changed branch and
|
|
25
|
+
* `handleRefusal`'s note merge), never while a revision is still owed: a merge that leaves
|
|
26
26
|
* `state.revision` set — a per-role user-request tick did not apply the rejected change — did
|
|
27
27
|
* not resolve it, and its next revision tick still needs the objections (loop-tick.ts's revision
|
|
28
|
-
* note). A model-reviewed
|
|
28
|
+
* note). A model-reviewed merge already overwrote `lastReview` with its approve; this only
|
|
29
29
|
* clears a standing `reject`. */
|
|
30
30
|
export function clearSupersededRejection(s) {
|
|
31
31
|
if (s.revision === undefined && s.lastReview?.verdict === "reject")
|
|
@@ -33,7 +33,7 @@ export function clearSupersededRejection(s) {
|
|
|
33
33
|
}
|
|
34
34
|
/** Consecutive failed ticks after which the loop raises a harness warning and its state
|
|
35
35
|
* cell reads "failing" (BUGS.md 2026-09-15: every loop failing identically looked like a
|
|
36
|
-
* quiet
|
|
36
|
+
* quiet team). Small on purpose — at 3 the error ladder has already doubled twice and
|
|
37
37
|
* the failure is clearly not transient. */
|
|
38
38
|
export const ERROR_STREAK_WARN = 3;
|
|
39
39
|
/** Consecutive failed ticks after which the error-streak circuit breaker (src/gates/streak-gate.ts)
|
|
@@ -53,7 +53,7 @@ const CUT_OFF_RESUME_LIMIT = 3;
|
|
|
53
53
|
* and takes a fresh tick on the idle ladder. A session the backend defers past the quiet
|
|
54
54
|
* watchdog every attempt would otherwise re-send the same input immediately and forever,
|
|
55
55
|
* burning an hour of slot time per retry with no output (BUGS.md 2026-09-18: 41 quiet kills
|
|
56
|
-
* consumed 44% of the
|
|
56
|
+
* consumed 44% of the team over two days). Mirrors CUT_OFF_RESUME_LIMIT; the streak itself
|
|
57
57
|
* keeps counting (LoopState.quietKillStreak) so one warning per episode can be raised. */
|
|
58
58
|
export const QUIET_KILL_RESUME_LIMIT = 3;
|
|
59
59
|
/** Record a finished tick on the loop's state and schedule its next run from the outcome.
|
|
@@ -61,30 +61,31 @@ export const QUIET_KILL_RESUME_LIMIT = 3;
|
|
|
61
61
|
* promptly, which back off and on which ladder, how cut-off resumes are bounded — sits with
|
|
62
62
|
* the other scheduling helpers here instead of inline in the tick lifecycle. Mutates `s` in place:
|
|
63
63
|
* the caller's state object is authoritative across an in-flight tick (see resetCounters).
|
|
64
|
-
* `cfg` is the role-resolved config (configForRole): its
|
|
64
|
+
* `cfg` is the role-resolved config (configForRole): its minRunIntervalSeconds carries any
|
|
65
65
|
* per-role slow clock, and idleBackoff passes through it unchanged from the top level. */
|
|
66
66
|
export function applyTickOutcome(s, cfg, role, outcome) {
|
|
67
67
|
s.running = false;
|
|
68
68
|
s.parkedSince = undefined;
|
|
69
|
+
s.parkedOn = undefined;
|
|
69
70
|
// The cut-off streak counts EVERY consecutive tick truncated at the context ceiling, past the
|
|
70
71
|
// resume limit too: the next fresh tick's prompt names how many attempts the window has eaten
|
|
71
72
|
// (buildCutOffNote), so the count must not freeze at the limit. Any other outcome resets it.
|
|
72
73
|
s.cutOffStreak = outcome.cutOff ? (s.cutOffStreak ?? 0) + 1 : 0;
|
|
73
74
|
// The error streak counts consecutive failed ticks; any other result resets it
|
|
74
75
|
// (BUGS.md 2026-09-15: 44 identical failures raised no alarm). A tick whose leftover
|
|
75
|
-
// recovery re-queued a pin its last
|
|
76
|
+
// recovery re-queued a pin its last merge failed counts too, even though the tick
|
|
76
77
|
// itself ends `queued` (outcome.recoveryFailure): a dead reviewer backend would otherwise
|
|
77
78
|
// reset the streak every tick and never raise the alarm (BUGS.md 2026-09-21).
|
|
78
79
|
// tick-finalize.ts emits one warning when the streak crosses ERROR_STREAK_WARN — once per
|
|
79
80
|
// episode, since the reset re-arms it — and the dashboards read "failing" from the same field.
|
|
80
81
|
// A plain `queued` tick is not yet a verdict about anything: it committed and enqueued, and
|
|
81
|
-
// its
|
|
82
|
+
// its MERGE resolves the streak — applyMergeOutcome resets it when the change merges and
|
|
82
83
|
// counts a review rejection into it (BUGS.md 2026-09-30: review-rejected authoring, the
|
|
83
|
-
// digest's #1 loss cause, fell straight through — the tick's `queued` reset and the
|
|
84
|
+
// digest's #1 loss cause, fell straight through — the tick's `queued` reset and the merge's
|
|
84
85
|
// `rejected` increment met nowhere, so the streak read 0 through the whole episode and the
|
|
85
86
|
// re-author ran at min interval, warning-free and pause-proof). Resetting here would pin a
|
|
86
87
|
// rejection streak at 1 forever, since every authoring tick in a rejection episode ends
|
|
87
|
-
// `queued` before its
|
|
88
|
+
// `queued` before its merge is decided.
|
|
88
89
|
const failed = outcome.result === "error" || outcome.recoveryFailure !== undefined;
|
|
89
90
|
if (failed)
|
|
90
91
|
s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
|
|
@@ -94,7 +95,16 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
94
95
|
// tick-finalize.ts warns once when it crosses QUIET_KILL_RESUME_LIMIT, and the branch below uses
|
|
95
96
|
// it to bound how many times a starved session is resumed (BUGS.md 2026-09-18).
|
|
96
97
|
s.quietKillStreak = outcome.result === "quiet_killed" ? (s.quietKillStreak ?? 0) + 1 : 0;
|
|
97
|
-
// The
|
|
98
|
+
// The no-change streak counts consecutive completed ticks that ended `no_change` (the
|
|
99
|
+
// resting state cell's ` · no change ×N` suffix — status-model.ts loopPhase). Any other
|
|
100
|
+
// completed result resets it, so a loop that merges between no-ops never shows a stale
|
|
101
|
+
// streak. Every completed result reaches this block (tick-finalize.ts calls
|
|
102
|
+
// applyTickOutcome for every outcome), so `changed`/`queued`/`error`/`aborted`/
|
|
103
|
+
// `quiet_killed`/`user_aborted` all reset it and only a `no_change` tick grows it.
|
|
104
|
+
// Observational only — never warns, unlike the error/quiet-kill streaks: a legitimately
|
|
105
|
+
// idle maintenance loop with an empty backlog can hold a long streak and that is expected.
|
|
106
|
+
s.noChangeStreak = outcome.result === "no_change" ? (s.noChangeStreak ?? 0) + 1 : 0;
|
|
107
|
+
// The yield ring records the result beside the streaks: a merge or a counted empty
|
|
98
108
|
// stretches the role's min-tick gap (yieldMultiplier, read by isEligible), an error-class
|
|
99
109
|
// result is skipped entirely — no evidence either way (yield-scaled clocks, PLANS.md).
|
|
100
110
|
pushYieldOutcome(s, outcome.result);
|
|
@@ -105,18 +115,18 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
105
115
|
// already committed.
|
|
106
116
|
if (outcome.result !== "aborted")
|
|
107
117
|
s.phase = undefined;
|
|
108
|
-
s.
|
|
118
|
+
s.lastRunEndedAt = Date.now();
|
|
109
119
|
// `lastResult`/`lastSummary` are the last COMPLETED result, the pair the dashboards' "last
|
|
110
120
|
// result" cell renders, and they must describe ONE tick: a completed outcome without a
|
|
111
121
|
// summary CLEARS `lastSummary`, so a summary-less result never renders beside another
|
|
112
122
|
// tick's work description (BUGS.md 2026-09-30: a no_change tick read "No change — Implement
|
|
113
|
-
// the … plan" beside work that had
|
|
123
|
+
// the … plan" beside work that had merged, because the prior summary was retained).
|
|
114
124
|
// A `queued` tick is not a completed result: its change is committed but in flight —
|
|
115
|
-
// the state column shows that (`
|
|
125
|
+
// the state column shows that (`merge <elapsed>`, the merge-queue badge) — so the pair
|
|
116
126
|
// keeps the prior outcome WITH its own summary while the change waits (BUGS.md 2026-09-23),
|
|
117
|
-
// and the tick's summary is stashed under the pinned sha for
|
|
118
|
-
// the
|
|
119
|
-
// queued or in-flight
|
|
127
|
+
// and the tick's summary is stashed under the pinned sha for applyMergeOutcome to pair with
|
|
128
|
+
// the merge's result. Every other outcome is completed and clears the stash: a role with a
|
|
129
|
+
// queued or in-flight merge never ticks (the orchestrator's interlock), so a stash that
|
|
120
130
|
// survives to a later tick names a change that is no longer waiting.
|
|
121
131
|
if (outcome.result === "queued") {
|
|
122
132
|
s.queuedSummary =
|
|
@@ -129,12 +139,12 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
129
139
|
}
|
|
130
140
|
if (outcome.result === "changed" || outcome.result === "queued") {
|
|
131
141
|
// "queued" schedules exactly like "changed" — the tick committed and enqueued — but the
|
|
132
|
-
// commit count waits for the
|
|
133
|
-
//
|
|
142
|
+
// commit count waits for the merge: `commits` keeps meaning "merged to main", and
|
|
143
|
+
// applyMergeOutcome increments it when the change actually merges. The phase-clear above
|
|
134
144
|
// (`result !== "aborted"`) covers both.
|
|
135
145
|
if (outcome.result === "changed")
|
|
136
146
|
s.commits += 1;
|
|
137
|
-
// A
|
|
147
|
+
// A merged change's patch-id approval has done its job: the same patch authored again
|
|
138
148
|
// later (after a revert, say) is a new change and gets its own review.
|
|
139
149
|
if (outcome.result === "changed")
|
|
140
150
|
s.lastApprovedPatchId = undefined;
|
|
@@ -146,7 +156,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
146
156
|
else if (outcome.result === "rejected") {
|
|
147
157
|
// The reviewer objected and the gate already reset the branch: the author should address
|
|
148
158
|
// the recorded reasons on its next eligible tick, not sleep through them — schedule like
|
|
149
|
-
// a change without counting a commit (nothing
|
|
159
|
+
// a change without counting a commit (nothing merged).
|
|
150
160
|
s.backoffSeconds = 0;
|
|
151
161
|
scheduleAtMinInterval(s, cfg);
|
|
152
162
|
}
|
|
@@ -205,7 +215,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
205
215
|
// value, so an error streak capped at the error ceiling never sleeps LESS than the loop
|
|
206
216
|
// already was sleeping, and the idle ladder picks up from there if the failures stop.
|
|
207
217
|
// A subscription claude usage-window rejection is a transient hold, not a failure of the
|
|
208
|
-
// session: the
|
|
218
|
+
// session: the team hold keeps ticks from starting until `resetsAt` (plans/agent-backends.md
|
|
209
219
|
// §8, part 6c), and the kept session and worktree edits resume when it lifts (part 6e).
|
|
210
220
|
// The named cause is what the bridge prompt reads, so it does not tell the session the
|
|
211
221
|
// harness merely restarted.
|
|
@@ -228,7 +238,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
228
238
|
else if (OBSERVER_ROLES.has(baseRoleOf(role))) {
|
|
229
239
|
// An observer's no_change is a success ("checked, all well"), not an idle verdict: it must
|
|
230
240
|
// not climb the idle ladder, which would punish the role monotonically for the product
|
|
231
|
-
// being healthy. With no ladder,
|
|
241
|
+
// being healthy. With no ladder, minRunIntervalSeconds is the sole, honest cadence
|
|
232
242
|
// (plans/observer-roles.md). The error/user_aborted arms above are untouched — a broken
|
|
233
243
|
// toolchain and a deliberate operator stop still back off like any other role.
|
|
234
244
|
s.backoffSeconds = 0;
|
|
@@ -243,7 +253,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
243
253
|
// outcome abandons them (the next fresh tick resets the worktree to main), so the hand-back
|
|
244
254
|
// is spent: clear the record, or it would make a later change's cap conflict discard the
|
|
245
255
|
// pin instead of handing it back (PLANS.md "Robust conflict landing, part 2/2"). `changed`
|
|
246
|
-
// clears too — the resolved change
|
|
256
|
+
// clears too — the resolved change merged in-tick, so its life is over. This runs after the
|
|
247
257
|
// resume branches above have set `resumePending`.
|
|
248
258
|
if (s.conflictHandback &&
|
|
249
259
|
outcome.result !== "queued" &&
|
|
@@ -251,71 +261,88 @@ export function applyTickOutcome(s, cfg, role, outcome) {
|
|
|
251
261
|
s.conflictHandback = undefined;
|
|
252
262
|
}
|
|
253
263
|
}
|
|
254
|
-
/** Record a completed
|
|
255
|
-
* the orchestrator calls this on the state object the
|
|
264
|
+
/** Record a completed MERGE on the authoring loop's state (plans/merge-queue.md 3/5):
|
|
265
|
+
* the orchestrator calls this on the state object the merge ran against — the runner's live
|
|
256
266
|
* copy when one exists, a disk load otherwise — then saves it, the same in-place discipline
|
|
257
267
|
* applyTickOutcome has (the caller's state object is authoritative; a role with a queued or
|
|
258
|
-
* in-flight
|
|
259
|
-
* to the
|
|
260
|
-
* counter applyTickOutcome's "queued" branch left to the
|
|
268
|
+
* in-flight merge never ticks, so no other writer holds it meanwhile). Sets `lastResult`
|
|
269
|
+
* to the merger's outcome, increments `commits` only on a success (the "merged to main"
|
|
270
|
+
* counter applyTickOutcome's "queued" branch left to the merge), and clears `phase` — the
|
|
261
271
|
* gate persisted `phase = "review"` on this same state object during the run, exactly as it
|
|
262
|
-
* did inside a tick, so the label must not linger past the
|
|
263
|
-
* applyTickOutcome's rule: every outcome except "aborted" clears it — an aborted
|
|
272
|
+
* did inside a tick, so the label must not linger past the merge (mirroring
|
|
273
|
+
* applyTickOutcome's rule: every outcome except "aborted" clears it — an aborted merge
|
|
264
274
|
* keeps it, because the interruption hit mid-review and the next launch must recover and
|
|
265
|
-
* re-review the pinned work). `lastSummary` becomes the summary of the change this
|
|
266
|
-
* resolved — `change` is its
|
|
275
|
+
* re-review the pinned work). `lastSummary` becomes the summary of the change this merge
|
|
276
|
+
* resolved — `change` is its merge-queue entry — so the merge's result, success or failure,
|
|
267
277
|
* reads beside the work it names (BUGS.md 2026-09-23: the queued tick held its summary back
|
|
268
278
|
* rather than pair it with the prior result). The tick's stashed `queuedSummary` wins when it
|
|
269
279
|
* names this sha: it is the text the tick reported, high-friction annotation included. The
|
|
270
280
|
* entry's own summary (the commit subject) covers a stash that is missing or names another
|
|
271
|
-
* change — a crash between enqueue and the tick's state save, or a
|
|
281
|
+
* change — a crash between enqueue and the tick's state save, or a merge that finished before
|
|
272
282
|
* its tick's outcome was applied. Either way the stash is consumed. The failure detail rides
|
|
273
|
-
* in `lastReview` and the
|
|
274
|
-
export function
|
|
283
|
+
* in `lastReview` and the merge_done/merge_failed events. */
|
|
284
|
+
export function applyMergeOutcome(s, result, change) {
|
|
275
285
|
s.lastResult = result;
|
|
276
286
|
s.lastSummary = s.queuedSummary?.sha === change.sha ? s.queuedSummary.summary : change.summary;
|
|
277
287
|
s.queuedSummary = undefined;
|
|
278
288
|
if (result === "changed") {
|
|
279
289
|
s.commits += 1;
|
|
290
|
+
// The last-merged timestamp: set only on a successful merge, so the rendered age means
|
|
291
|
+
// "time since the loop last actually merged to main" — a rejected/merge_conflict/aborted
|
|
292
|
+
// merge, like a no_change tick, leaves the prior value standing (PLANS.md "Each loop's
|
|
293
|
+
// last-landed age").
|
|
294
|
+
s.lastMergedAt = Date.now();
|
|
280
295
|
s.lastApprovedPatchId = undefined; // see applyTickOutcome
|
|
281
|
-
// A
|
|
282
|
-
// lastReview with its approve, but a review-exempt (or review-disabled)
|
|
296
|
+
// A merge supersedes a stale rejection: a model-reviewed merge has already overwritten
|
|
297
|
+
// lastReview with its approve, but a review-exempt (or review-disabled) merge records no
|
|
283
298
|
// verdict, so a stale reject would otherwise ride every later tick prompt (BUGS.md
|
|
284
299
|
// 2026-10-07). Cleared only with no revision pending — see clearSupersededRejection.
|
|
285
300
|
clearSupersededRejection(s);
|
|
286
|
-
// A
|
|
301
|
+
// A merged change resets the error streak exactly as an error-free tick does (BUGS.md
|
|
287
302
|
// 2026-09-30): the queued tick that authored it preserved the streak — a rejection counts
|
|
288
|
-
// into it below — so the
|
|
303
|
+
// into it below — so the merge, the episode's only clean verdict, ends it.
|
|
289
304
|
s.consecutiveErrors = 0;
|
|
290
305
|
}
|
|
291
306
|
if (result === "rejected") {
|
|
292
|
-
// A review rejection is a failure the streak must count (BUGS.md
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// the
|
|
296
|
-
//
|
|
307
|
+
// A review rejection is a failure the streak must count ONLY when it is final (BUGS.md
|
|
308
|
+
// 2026-10-14): recordRejectedChange (src/merge/merge-core.ts) already decided finality
|
|
309
|
+
// by the time this runs — it sets `state.revision` when the change goes back for another
|
|
310
|
+
// round and clears it when the chain is exhausted (or never sets it for a role that cannot
|
|
311
|
+
// revise, the director). A rejection that leaves a revision to work on is not a verdict
|
|
312
|
+
// about the author yet: counting every round of ONE change made an exhausted chain's third
|
|
313
|
+
// round cross the warn bar and flash a red "failing tick after tick" alert that needed
|
|
314
|
+
// nothing from the operator and was gone before it could be read (0–30 s). Counting only
|
|
315
|
+
// final rejections makes a streak of 3 mean three changes ended rejected (or errored) —
|
|
316
|
+
// the non-converging author the 2026-09-30 fix meant to catch — while a chain's rounds
|
|
317
|
+
// leave the streak unchanged and keep the fast minimum-interval retry the queued tick
|
|
318
|
+
// scheduled. This merge outcome is the accumulation point (the authoring tick ends
|
|
319
|
+
// `queued`, which no longer resets the streak, see applyTickOutcome) — the same field the
|
|
320
|
+
// warn, the breaker (src/gates/streak-gate.ts), and the error-storm observation
|
|
321
|
+
// (src/team/team-polls.ts) already read. No new machinery, no new constants: the warn
|
|
297
322
|
// bar and the breaker bar are ERROR_STREAK_WARN and ERROR_STREAK_BREAKER themselves.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
323
|
+
if (s.revision === undefined) {
|
|
324
|
+
s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
|
|
325
|
+
const lastReasons = s.lastReview?.verdict === "reject" ? s.lastReview.reasons : undefined;
|
|
326
|
+
s.lastError = `review rejected: ${firstReason(lastReasons).slice(0, 200)}`;
|
|
327
|
+
// Past the warn bar the re-author backs off on the error ladder instead of the minimum
|
|
328
|
+
// interval the queued tick scheduled: the fast first retries stay — the rejected branch's
|
|
329
|
+
// design intent, letting the author address the recorded reasons at once — but a
|
|
330
|
+
// non-converging author slows down instead of re-authoring at full price all day. The
|
|
331
|
+
// rung derives from the streak (not from backoffSeconds, which every queued tick zeroes
|
|
332
|
+
// on its way out, pinning a state-derived ladder at its first rung), and never pulls
|
|
333
|
+
// nextRunAt earlier than the already-scheduled minimum interval.
|
|
334
|
+
const streak = s.consecutiveErrors;
|
|
335
|
+
if (streak >= ERROR_STREAK_WARN) {
|
|
336
|
+
const rung = Math.min(ERROR_BACKOFF.initialSeconds * 2 ** (streak - ERROR_STREAK_WARN), ERROR_BACKOFF.maxSeconds);
|
|
337
|
+
s.backoffSeconds = rung;
|
|
338
|
+
s.nextRunAt = Math.max(s.nextRunAt ?? 0, Date.now() + rung * 1000);
|
|
339
|
+
}
|
|
313
340
|
}
|
|
314
341
|
}
|
|
315
|
-
// A
|
|
342
|
+
// A merge or a rejection is the end of the change's life; a merge_conflict keeps its pin.
|
|
316
343
|
// The per-change markers below all clear on this one outcome set, so name it once.
|
|
317
344
|
const changeGone = result === "changed" || result === "rejected";
|
|
318
|
-
// The conflict streak counts failed resolutions of one pinned sha; a
|
|
345
|
+
// The conflict streak counts failed resolutions of one pinned sha; a merge or a rejection
|
|
319
346
|
// ends that pin's life. Every other outcome kept the pin as it was, so its count stands.
|
|
320
347
|
if (result === "merge_conflict") {
|
|
321
348
|
const prior = s.mergeConflicts?.sha === change.sha ? s.mergeConflicts.count : 0;
|
|
@@ -324,13 +351,15 @@ export function applyLandingOutcome(s, result, change) {
|
|
|
324
351
|
else if (changeGone) {
|
|
325
352
|
s.mergeConflicts = undefined;
|
|
326
353
|
}
|
|
327
|
-
// Likewise the red-
|
|
328
|
-
if (changeGone)
|
|
329
|
-
s.
|
|
330
|
-
|
|
354
|
+
// Likewise the red-merge-check streak (keyed by patch-id): its change is gone either way.
|
|
355
|
+
if (changeGone) {
|
|
356
|
+
s.mergeCheckFailures = undefined;
|
|
357
|
+
s.mergeFlakeExcused = undefined;
|
|
358
|
+
}
|
|
359
|
+
// A permanent-reviewer-config hold ends with its pin's life: merged, or rejected away.
|
|
331
360
|
if (changeGone)
|
|
332
|
-
s.
|
|
333
|
-
// A hand-back ends with its change's life too: once the resolved change
|
|
361
|
+
s.mergeReviewError = undefined;
|
|
362
|
+
// A hand-back ends with its change's life too: once the resolved change merges or is rejected,
|
|
334
363
|
// a later change gets its own one-hand-back budget (PLANS.md "Robust conflict landing, part
|
|
335
364
|
// 2/2"). A merge_conflict keeps it, so the resolved change's own cap discard fires.
|
|
336
365
|
if (changeGone)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Collection half of `tumwater tick <role> <n>`: one completed tick's full event block,
|
|
2
3
|
* distilled from the event log into a TickDetail payload. The collector-in-core convention
|
|
3
4
|
* beside history-data.ts and src/report/report-data.ts — a pure read over root, so core
|
|
@@ -7,58 +8,58 @@
|
|
|
7
8
|
* Every datum rides the events the harness already writes, so this is a distillation of the
|
|
8
9
|
* existing record, not a new one. The rendering half — the summary header, formatEvent, the
|
|
9
10
|
* CLI command — lives in tick-detail.ts. */
|
|
10
|
-
import { HISTORY_SCAN_MAX_EVENTS } from "../history/history-data.js";
|
|
11
|
+
import { HISTORY_SCAN_MAX_EVENTS, bucketMergeEvents, resolveQueuedResult } from "../history/history-data.js";
|
|
11
12
|
import { eventUsage, readEvents, tickSpanMs, tickStartMap } from "../events/event-read.js";
|
|
12
13
|
import { outcomeText, usageText } from "../events/event-format.js";
|
|
13
14
|
/** The event block of `role`'s tick `n`, or null when the scanned window holds no such tick.
|
|
14
|
-
* The newest `
|
|
15
|
+
* The newest `run_start` the log retains for `${role}#${n}` wins (a counter reset can make an
|
|
15
16
|
* old tick number recur; the operator asking by number means the most recent one), and the
|
|
16
|
-
* block closes at the first `
|
|
17
|
-
* missing (in flight, or its tail lost to rotation), at the loop's next `
|
|
17
|
+
* block closes at the first `run_end` for that tick after the start — or, when that end is
|
|
18
|
+
* missing (in flight, or its tail lost to rotation), at the loop's next `run_start`, so a
|
|
18
19
|
* rotation-cut block never leaks the following tick's events into this one. When even the
|
|
19
|
-
* start is gone but a matching `
|
|
20
|
+
* start is gone but a matching `run_end` survives, the end-alone block still answers with
|
|
20
21
|
* `startTs` null and a null duration (the unpaired rule); nothing at all in the scan is the
|
|
21
22
|
* not-found case the caller renders as a not-found line. */
|
|
22
23
|
export function readTickDetail(root, role, tick) {
|
|
23
24
|
// The scan rides history-data.ts's shared one-ask ceiling (HISTORY_SCAN_MAX_EVENTS): one
|
|
24
|
-
// tick's block is small, but it can sit arbitrarily far back in a long-lived
|
|
25
|
+
// tick's block is small, but it can sit arbitrarily far back in a long-lived team's log,
|
|
25
26
|
// and readTailText reads bytes proportional to the limit's line count — so the cap is also
|
|
26
27
|
// the largest byte read one command can cost, and every read-only view scans under the
|
|
27
28
|
// same bound.
|
|
28
|
-
const events = readEvents(root, HISTORY_SCAN_MAX_EVENTS).filter((e) => e.
|
|
29
|
-
// Newest
|
|
29
|
+
const events = readEvents(root, HISTORY_SCAN_MAX_EVENTS).filter((e) => e.source === role);
|
|
30
|
+
// Newest run_start for this loop+tick: the block's lower bound.
|
|
30
31
|
let startIdx = -1;
|
|
31
32
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
32
33
|
const e = events[i];
|
|
33
|
-
if (e.type === "
|
|
34
|
+
if (e.type === "run_start" && e.run === tick) {
|
|
34
35
|
startIdx = i;
|
|
35
36
|
break;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
if (startIdx < 0) {
|
|
39
|
-
// The start is gone (rotation) or the tick never ran. A surviving
|
|
40
|
+
// The start is gone (rotation) or the tick never ran. A surviving run_end for the same
|
|
40
41
|
// number still answers — unpaired, undated at the front — else there is no such tick here.
|
|
41
42
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
42
43
|
const e = events[i];
|
|
43
|
-
if (e.type === "
|
|
44
|
+
if (e.type === "run_end" && e.run === tick)
|
|
44
45
|
return detailFrom(role, tick, events, [e], e, null);
|
|
45
46
|
}
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
const start = events[startIdx];
|
|
49
|
-
// The block's upper bound: the first
|
|
50
|
-
// later
|
|
50
|
+
// The block's upper bound: the first run_end for this tick after the start ends it; a
|
|
51
|
+
// later run_start (this tick's end lost to rotation) closes the block exclusive — the
|
|
51
52
|
// events after it belong to the next tick, not this one. No end and no next start reads as
|
|
52
53
|
// in flight: everything since the start belongs to the open tick.
|
|
53
54
|
let end = null;
|
|
54
55
|
let nextStart = null;
|
|
55
56
|
for (let i = startIdx + 1; i < events.length; i++) {
|
|
56
57
|
const e = events[i];
|
|
57
|
-
if (e.type === "
|
|
58
|
+
if (e.type === "run_end" && e.run === tick) {
|
|
58
59
|
end = e;
|
|
59
60
|
break;
|
|
60
61
|
}
|
|
61
|
-
if (e.type === "
|
|
62
|
+
if (e.type === "run_start") {
|
|
62
63
|
nextStart = e;
|
|
63
64
|
break;
|
|
64
65
|
}
|
|
@@ -74,21 +75,34 @@ export function readTickDetail(root, role, tick) {
|
|
|
74
75
|
}
|
|
75
76
|
return detailFrom(role, tick, events, block, end, start.ts);
|
|
76
77
|
}
|
|
77
|
-
/** Fill the payload around a collected block: usage from the
|
|
78
|
+
/** Fill the payload around a collected block: usage from the run_end via eventUsage/usageText,
|
|
78
79
|
* duration via tickSpanMs against the loop's own start map (the shared pairing helper, so this
|
|
79
80
|
* view and the history rows cannot drift on what a span is), events oldest-first. */
|
|
80
81
|
function detailFrom(role, tick, scoped, block, end, startTs) {
|
|
81
82
|
const usageNums = end === null ? { tokens: 0, costUsd: 0 } : eventUsage(end);
|
|
83
|
+
let result = end === null ? null : outcomeText(end);
|
|
84
|
+
let merge = null;
|
|
85
|
+
if (end !== null && result === "queued") {
|
|
86
|
+
// A fresh claim cursor suits one tick: the walk starts at the newest pin at or before this
|
|
87
|
+
// tick's end, which is its own pin, since later ticks' pins sit after it.
|
|
88
|
+
const { mergeQueuedByLoop, outcomeByLoop } = bucketMergeEvents(scoped);
|
|
89
|
+
const resolved = resolveQueuedResult(end, mergeQueuedByLoop, outcomeByLoop, new Map(), tickStartMap(scoped), null);
|
|
90
|
+
if (resolved !== null) {
|
|
91
|
+
result = resolved.result;
|
|
92
|
+
merge = resolved.outcome;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
return {
|
|
83
96
|
role,
|
|
84
|
-
tick,
|
|
97
|
+
run: tick,
|
|
85
98
|
startTs,
|
|
86
99
|
endTs: end === null ? null : end.ts,
|
|
87
100
|
durationMs: end === null ? null : tickSpanMs(end, tickStartMap(scoped)),
|
|
88
|
-
result
|
|
101
|
+
result,
|
|
89
102
|
tokens: usageNums.tokens,
|
|
90
103
|
costUsd: usageNums.costUsd,
|
|
91
104
|
usage: end === null ? "" : usageText(end),
|
|
92
105
|
events: block,
|
|
106
|
+
merge,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Rendering + command half of `tumwater tick <role> <n>`: one completed tick's full event
|
|
2
3
|
* block printed as a short summary header followed by the tick's events through the shared
|
|
3
4
|
* formatEvent. The payload itself is collected by tick-detail-data.ts (the collector-in-core
|
|
@@ -10,13 +11,17 @@ import { knownRoleIdsCached } from "../config/config.js";
|
|
|
10
11
|
import { readTickRows } from "../history/history-data.js";
|
|
11
12
|
import { readTickDetail } from "./tick-detail-data.js";
|
|
12
13
|
import { formatEvent } from "../events/event-format.js";
|
|
14
|
+
import { eventTypeLabel } from "../events/event-labels.js";
|
|
15
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
13
16
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
14
17
|
import { shortSpanPhrase } from "../text/phrases.js";
|
|
15
18
|
import { shortSha } from "../text/format.js";
|
|
19
|
+
import { gitTry } from "../git/git-run.js";
|
|
20
|
+
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
16
21
|
/** The tick command's synopsis, word for word what cli.ts's dispatcher gate and cmdTick's own
|
|
17
22
|
* arity and unknown-role guards fail with — one string so the three sites cannot drift apart
|
|
18
23
|
* when the command's shape changes. */
|
|
19
|
-
export const TICK_USAGE = "tumwater
|
|
24
|
+
export const TICK_USAGE = "tumwater history <role> [<n>] [--last] [--stat] [--json]";
|
|
20
25
|
/** The shared not-found frame both wordings complete — what the scan looked for, the loop,
|
|
21
26
|
* and that the scanned window (not the world) is what came up empty. Exactly two consumers,
|
|
22
27
|
* the not-found wordings below (the numbered form's exported message and the --last form's
|
|
@@ -25,41 +30,76 @@ function tickNotFoundPhrase(role, what) {
|
|
|
25
30
|
return `no ${what} for ${role} in the scanned window (the retained log may have rotated past it)`;
|
|
26
31
|
}
|
|
27
32
|
/** The not-found wording a missed tick lookup owes its surface — `tumwater tick <role> <n>`'s
|
|
28
|
-
* stdout line and the GUI /api/
|
|
33
|
+
* stdout line and the GUI /api/run endpoint's 404 JSON error, one template so the CLI's prose
|
|
29
34
|
* and the browser's error card cannot drift when the phrasing moves. Names the shape a user
|
|
30
35
|
* can act on: the tick number, the loop, and that the scanned window (not the world) is what
|
|
31
36
|
* came up empty. */
|
|
32
37
|
export function tickNotFoundMessage(role, tick) {
|
|
33
|
-
return tickNotFoundPhrase(role, `
|
|
38
|
+
return tickNotFoundPhrase(role, `run #${tick}`);
|
|
34
39
|
}
|
|
35
40
|
/** The not-found wording for `tumwater tick <role> --last`, sibling of tickNotFoundMessage —
|
|
36
41
|
* the same surfaces but naming what the last-form scan actually asked for: the newest
|
|
37
|
-
* completed tick, not a number the user supplied. Module-local: the GUI's /api/
|
|
42
|
+
* completed tick, not a number the user supplied. Module-local: the GUI's /api/run 404 only
|
|
38
43
|
* ever serves the numbered form, so this wording has no second consumer. */
|
|
39
44
|
function tickNotFoundLastMessage(role) {
|
|
40
|
-
return tickNotFoundPhrase(role, "completed
|
|
45
|
+
return tickNotFoundPhrase(role, "completed run");
|
|
46
|
+
}
|
|
47
|
+
/** The commit a tick pinned: the `commit` of the first `merge_queued`/`merge_done` event in its
|
|
48
|
+
* block carrying a non-empty one, else null. The one rule both the header's sha and --stat use. */
|
|
49
|
+
function tickCommitSha(d) {
|
|
50
|
+
const queued = d.events.find((e) => (e.type === "merge_queued" || e.type === "merge_done") && typeof e.commit === "string" && e.commit !== "");
|
|
51
|
+
return queued === undefined ? null : queued.commit;
|
|
52
|
+
}
|
|
53
|
+
/** The per-file `git show --stat` summary of a tick's pinned commit. The sha is the authored
|
|
54
|
+
* commit the tick pinned: a merge that rebased gives main a different sha with the same
|
|
55
|
+
* changes, so this describes what the tick changed. */
|
|
56
|
+
async function tickCommitStat(root, sha) {
|
|
57
|
+
if (sha === null)
|
|
58
|
+
return "(no commit: this run sent nothing to merge)";
|
|
59
|
+
const out = await gitTry(root, "show", "--stat", "--format=", sha);
|
|
60
|
+
if (out === null)
|
|
61
|
+
return `(commit ${shortSha(sha)} is not in this repository)`;
|
|
62
|
+
return out.trimEnd();
|
|
63
|
+
}
|
|
64
|
+
/** The pinned commit's stat text for a tick, shared by `tick --stat` and GET /api/run so both
|
|
65
|
+
* apply one rule for the sha and the "(no commit …)" / "(commit … is not in this repository)"
|
|
66
|
+
* texts. */
|
|
67
|
+
export function tickDetailStat(root, d) {
|
|
68
|
+
return tickCommitStat(root, tickCommitSha(d));
|
|
41
69
|
}
|
|
42
70
|
/** The human view of a TickDetail: one summary line — result (or the in-flight/unpaired
|
|
43
|
-
* marker), duration, usage, and the pinned commit sha when the block's `
|
|
71
|
+
* marker), duration, usage, and the pinned commit sha when the block's `merge_queued`/`merge_done`
|
|
44
72
|
* event carries one — then each event through formatEvent, the shared renderer `logs` and the
|
|
45
|
-
* TUI use, so the trail reads exactly like the feed it came from.
|
|
73
|
+
* TUI use, so the trail reads exactly like the feed it came from. A queued tick whose merge
|
|
74
|
+
* has finished shows that verdict in the header and ends with the merge's outcome line; a
|
|
75
|
+
* leftover-recovery run's verdict is the failed attempt it re-queued (resolveQueuedResult),
|
|
76
|
+
* logged before the run began, so that outcome line leads the trail instead — the trail stays
|
|
77
|
+
* in time order. */
|
|
46
78
|
export function renderTickDetail(d) {
|
|
47
79
|
const summary = [];
|
|
48
80
|
if (d.endTs === null)
|
|
49
81
|
summary.push(d.startTs === null ? "unpaired" : "in flight");
|
|
50
82
|
else
|
|
51
|
-
summary.push(d.result
|
|
83
|
+
summary.push(d.result ? eventTypeLabel(d.result) : "?");
|
|
52
84
|
if (d.durationMs !== null)
|
|
53
85
|
summary.push(shortSpanPhrase(d.durationMs));
|
|
54
86
|
if (d.usage !== "")
|
|
55
87
|
summary.push(d.usage);
|
|
56
|
-
const
|
|
57
|
-
let header = `${d.role}
|
|
58
|
-
if (
|
|
59
|
-
header += ` · commit ${shortSha(
|
|
60
|
-
|
|
88
|
+
const sha = tickCommitSha(d);
|
|
89
|
+
let header = `${roleLabel(d.role)} run #${d.run} — ${summary.join(" · ")}`;
|
|
90
|
+
if (sha !== null)
|
|
91
|
+
header += ` · commit ${shortSha(sha)}`;
|
|
92
|
+
const trail = d.events.map(formatEvent);
|
|
93
|
+
if (d.merge !== null) {
|
|
94
|
+
const first = d.events[0];
|
|
95
|
+
if (first !== undefined && d.merge.ts < first.ts)
|
|
96
|
+
trail.unshift(formatEvent(d.merge));
|
|
97
|
+
else
|
|
98
|
+
trail.push(formatEvent(d.merge));
|
|
99
|
+
}
|
|
100
|
+
return [header, ...trail].join("\n");
|
|
61
101
|
}
|
|
62
|
-
/** `tumwater tick <role> [<n>] [--last] [--json]`: print one completed tick's full event
|
|
102
|
+
/** `tumwater tick <role> [<n>] [--last] [--stat] [--json]`: print one completed tick's full event
|
|
63
103
|
* trail. Read-only: stdout only, no state file created — a tick the scan cannot find prints a
|
|
64
104
|
* not-found line and exits 0 (history's empty-output convention: an absent record is an
|
|
65
105
|
* answer, not a failure), and under --json it prints the JSON document `null` instead — the
|
|
@@ -69,7 +109,7 @@ export function renderTickDetail(d) {
|
|
|
69
109
|
* numbered form would — the summary header and event rendering are shared, only the number's
|
|
70
110
|
* provenance differs (readTickRows' newest-first scan, not the user's argument). --last and a
|
|
71
111
|
* numeric <n> are rivals: exactly one positional under --last, exactly two without. */
|
|
72
|
-
export async function cmdTick(root, positionals, json, last = false) {
|
|
112
|
+
export async function cmdTick(root, positionals, json, last = false, stat = false) {
|
|
73
113
|
// Positional arity by form: cli.ts has already peeled the flags off and gated them (only
|
|
74
114
|
// --json and --last are admitted), so anything left over that does not fit the chosen form
|
|
75
115
|
// is a mistake. --last takes exactly one positional (<role>); the numeric path keeps
|
|
@@ -77,7 +117,7 @@ export async function cmdTick(root, positionals, json, last = false) {
|
|
|
77
117
|
const arity = last ? 1 : 2;
|
|
78
118
|
if (positionals.length !== arity)
|
|
79
119
|
fail(`usage: ${TICK_USAGE}`);
|
|
80
|
-
const role = positionals[0];
|
|
120
|
+
const role = canonicalRoleId(positionals[0]);
|
|
81
121
|
// The role is validated like every role-targeting command's --role (knownRoleIdsCached: the
|
|
82
122
|
// built-ins plus user-defined loops, read through the never-throwing cached loader so a
|
|
83
123
|
// transiently broken tumwater.json cannot take a read-only view down), with the shared
|
|
@@ -91,8 +131,8 @@ export async function cmdTick(root, positionals, json, last = false) {
|
|
|
91
131
|
// number, read from readTickRows' newest-first scan for the role. An empty scan is not
|
|
92
132
|
// found by construction, so the not-found path below takes it with the last-form wording.
|
|
93
133
|
const tick = last
|
|
94
|
-
? (readTickRows(root, 1, role)[0]?.
|
|
95
|
-
: parseCountFlag("
|
|
134
|
+
? (readTickRows(root, 1, role)[0]?.run ?? null)
|
|
135
|
+
: parseCountFlag("history <n>", positionals[1]);
|
|
96
136
|
const detail = tick === null ? null : readTickDetail(root, role, tick);
|
|
97
137
|
if (tick === null) {
|
|
98
138
|
// The --last scan came up empty: not found by construction, the last-form wording.
|
|
@@ -106,7 +146,7 @@ export async function cmdTick(root, positionals, json, last = false) {
|
|
|
106
146
|
// Not found stays exit 0 in both renderings, but --json still owes a parseable document in
|
|
107
147
|
// every exit-0 case (the report --json precedent, the invariant this command's own doc
|
|
108
148
|
// comment claims): `null` is the JSON answer for a single-record lookup that missed — the
|
|
109
|
-
// prose line would end a jq pipe mid-
|
|
149
|
+
// prose line would end a jq pipe mid-team-incident.
|
|
110
150
|
if (json)
|
|
111
151
|
sayJson(null);
|
|
112
152
|
else
|
|
@@ -115,8 +155,9 @@ export async function cmdTick(root, positionals, json, last = false) {
|
|
|
115
155
|
}
|
|
116
156
|
// --json swaps the renderer for the collector's own payload, exactly as history --json: a
|
|
117
157
|
// JSON document in every exit-0 case, never prose.
|
|
158
|
+
const statText = stat ? await tickDetailStat(root, detail) : null;
|
|
118
159
|
if (json)
|
|
119
|
-
sayJson(detail);
|
|
160
|
+
sayJson(statText === null ? detail : { ...detail, stat: statText });
|
|
120
161
|
else
|
|
121
|
-
say(renderTickDetail(detail));
|
|
162
|
+
say(statText === null ? renderTickDetail(detail) : `${renderTickDetail(detail)}\n\n${statText}`);
|
|
122
163
|
}
|