tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { shortSha } from "../text/format.js";
|
|
2
|
-
import {
|
|
2
|
+
import { NEEDS_REVIEW_PREFIX, NEEDS_REVIEW_NOTE, CHECK_DECLARATION_MARKER } from "../roles/role-guidance.js";
|
|
3
3
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
4
4
|
import { formatTimestamp } from "../text/datetime.js";
|
|
5
5
|
import { numberedList } from "../text/markdown.js";
|
|
@@ -29,12 +29,12 @@ export function buildRejectedReviewNote(review, role) {
|
|
|
29
29
|
}
|
|
30
30
|
// On exhaustion the rejected diff is gone. Feature's rejected change often implemented a
|
|
31
31
|
// PLANS.md entry, and re-authoring the same plan from scratch is what produced the long
|
|
32
|
-
// rejection runs this note exists to end, so feature is sent to
|
|
33
|
-
// A plan-less role has no downstream role to replan for it,
|
|
34
|
-
// notes instead: it records the abandoned purpose and any instances
|
|
35
|
-
// unhandled, then takes a different theme on the next run. Re-authoring the same
|
|
36
|
-
// from the same objections is what restarted the round and produced the long rejection
|
|
37
|
-
// runs this note exists to end. The markdown-only
|
|
32
|
+
// rejection runs this note exists to end, so feature is sent to write a Needs-review note so
|
|
33
|
+
// the plan role rewrites the entry. A plan-less role has no downstream role to replan for it,
|
|
34
|
+
// so it is sent to its own notes instead: it records the abandoned purpose and any instances
|
|
35
|
+
// that remain unhandled, then takes a different theme on the next run. Re-authoring the same
|
|
36
|
+
// purpose from the same objections is what restarted the round and produced the long rejection
|
|
37
|
+
// runs this note exists to end. The markdown-only Needs-review note merges review-exempt, but a
|
|
38
38
|
// merged change clears the standing rejection (clearSupersededRejection, run-apply.ts), so
|
|
39
39
|
// the instruction does not re-inject once the note has merged. `role` is the runner id, so
|
|
40
40
|
// feature's instances (`feature-2`, …) are matched through their base role: matched bare,
|
|
@@ -44,7 +44,7 @@ export function buildRejectedReviewNote(review, role) {
|
|
|
44
44
|
if (review.exhausted) {
|
|
45
45
|
finality =
|
|
46
46
|
role !== undefined && baseRoleOf(role) === "feature"
|
|
47
|
-
? ` This was the change's final revision round — the same objection survived two revisions (or the chain hit its cap), so the rejected diff and ref are gone. If it implemented a PLANS.md entry, do not re-author it: append ${
|
|
47
|
+
? ` This was the change's final revision round — the same objection survived two revisions (or the chain hit its cap), so the rejected diff and ref are gone. If it implemented a PLANS.md entry, do not re-author it: append ${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds** under that entry's heading, with the numbered objections above quoted beneath it, and merge that markdown-only change and nothing else so the plan role can rewrite the plan. Otherwise, redo the change against current main.`
|
|
48
48
|
: " This was the change's final revision round — the same objection survived two revisions (or the chain hit its cap), so the rejected diff and ref are gone. Do not re-author it. Record the abandoned purpose and any instances of it that remain unhandled in your notes, then take a different theme on this run: re-deriving the same purpose from the same objections is what produced the long rejection runs this note exists to end.";
|
|
49
49
|
}
|
|
50
50
|
const tail = finality === "" ? "" : `\n${finality.trim()}`;
|
|
@@ -106,6 +106,25 @@ Sibling instances hold these backlog entries; do not pick, edit, or implement th
|
|
|
106
106
|
${titles.map((t) => `- "${t}"`).join("\n")}
|
|
107
107
|
</held-entries>`;
|
|
108
108
|
}
|
|
109
|
+
/** The note injected into a `plan` or `feature` run when the project has no declared
|
|
110
|
+
* deterministic check (detectBuildCheck returned null) and the team should close the gap itself
|
|
111
|
+
* — the plan role writes the declaration entry, the feature role implements it. Exactly one of
|
|
112
|
+
* the two notes is in flight, decided by CHECK_DECLARATION_MARKER rather than prose (see
|
|
113
|
+
* declarationPlanEntry, src/backlog/backlog.ts): no marker → the plan note (write the
|
|
114
|
+
* declaration); marker → the feature note (implement it). run-prompt.ts picks which note to
|
|
115
|
+
* render from the base role and whether a marker entry waits, so a work instance (`feature-2`, …)
|
|
116
|
+
* matches through its base role and the marker test decides the rest. A pure function so its
|
|
117
|
+
* shape is pinned in tests. */
|
|
118
|
+
export function buildNoCheckNote(role) {
|
|
119
|
+
if (baseRoleOf(role) === "plan") {
|
|
120
|
+
return `<no-check>
|
|
121
|
+
This project declares no deterministic check, so the safety gates are off. Before anything else, write the entry that declares one: headed \`### Declare the project's deterministic check\`, marked on its own line with ${CHECK_DECLARATION_MARKER}, naming the exact command and the route it lands by — a \`test\`/\`typecheck\`/\`build\` script added to package.json when the walked-up install root carries one, else \`check.command\` — with acceptance criteria that the doctor's \`check\` row reads ok and the \`no-check\` alert is gone. Writing it is this run's work even when the eligible-plan target is already met, so the in-prompt queue-full stop yields to it; a full plan backlog defers the plan role before this prompt is built, so this note only reaches a run already admitted past that stop. A Needs-review lift and an assigned claim still come first. A project with nothing to verify yet — no source, no script to wire — gets no plan: end with ${NOTHING_TO_DO} instead.
|
|
122
|
+
</no-check>`.trim();
|
|
123
|
+
}
|
|
124
|
+
return `<no-check>
|
|
125
|
+
The PLANS.md \`## Planned\` entry marked ${CHECK_DECLARATION_MARKER} is this run's task unless the run was handed another entry: implement its declaration so a merge makes it live — the doctor's \`check\` row reads ok and \`tumwater status --json\` carries no \`noDeclaredCheck\`. Do not chase an in-run re-read, though: while the change is unmerged, both still reflect the pre-declaration state, so live is confirmed when the change merges, not in this run — mark the plan done when the declaration is complete and correct, and let the merge confirm it. Choose the route the repo takes: (1) add the verifying script to package.json as \`test\`/\`typecheck\`/\`build\` (detection's preference order), a tracked diff the reviewer sees, live the moment it merges; (2) otherwise write \`{ "check": { "command": "…" } }\` to \`.tumwater-config-request.json\` at the worktree root, never tumwater.json — the request is run green before it is applied, then the file is deleted. A marker entry held with a Needs-review note is not available to you, so end the run there rather than forcing it.
|
|
126
|
+
</no-check>`.trim();
|
|
127
|
+
}
|
|
109
128
|
/** The longest title an entry list quotes: a PLANS.md heading's tail names the plan's dates and
|
|
110
129
|
* every file it touches, which its range already leads to. */
|
|
111
130
|
const LISTED_TITLE_CHARS = 100;
|
|
@@ -150,9 +169,9 @@ export function buildConflictDiscardNote(summary, attempts) {
|
|
|
150
169
|
}
|
|
151
170
|
/** The note injected into the `ci` healer's prompt when main's own suite is red (PLANS.md
|
|
152
171
|
* "Red-main handoff"): the gate's warning event is harness-level and no role prompt reads it, so
|
|
153
|
-
* the
|
|
154
|
-
* run's
|
|
155
|
-
*
|
|
172
|
+
* the healer — whose job is to make main green — is told what failed and that fixing it is this
|
|
173
|
+
* run's task. The healer's normal charter is a starting point, not a fit — a red main keeps the
|
|
174
|
+
* whole team working on it, and the gate's deterministic pre-check rejects any change that leaves
|
|
156
175
|
* the suite red, so authoring anything else first is waste. A pure function so its shape is
|
|
157
176
|
* pinned in tests; script/headline are optional because a red carries a failing script, but the
|
|
158
177
|
* note must still read sensibly if it is ever absent. */
|
|
@@ -172,22 +191,28 @@ export function buildMainRedNote(sha, script, headline, tail) {
|
|
|
172
191
|
? `\nFailing check output (${output.length} lines):\n\`\`\`\n${output.join("\n")}\n\`\`\`\n`
|
|
173
192
|
: "";
|
|
174
193
|
return `<main-red>
|
|
175
|
-
main's own suite is red at ${shortSha(sha)} (${what}). Every
|
|
194
|
+
main's own suite is red at ${shortSha(sha)} (${what}). Every role keeps working on the red main, but no change that leaves the suite red merges until main is green.
|
|
176
195
|
This run your job is to make main green: reproduce that failure, fix it (add a regression test where one is feasible), and merge the fix.
|
|
177
196
|
The review gate's deterministic pre-check runs main + your change, so a change that leaves the suite red is rejected — do not author anything else first.
|
|
178
197
|
If the failure is environmental — flaky, load-sensitive, or a broken local toolchain — say so plainly in your reply rather than editing unrelated code.
|
|
179
198
|
${tailBlock}</main-red>`;
|
|
180
199
|
}
|
|
181
|
-
/** The note every role but
|
|
182
|
-
*
|
|
183
|
-
*
|
|
200
|
+
/** The note injected into every role's prompt but the `ci` healer's own while main is red with
|
|
201
|
+
* named failures (the `<main-known-red>` block part 3a/3 dropped with the red-main authoring
|
|
202
|
+
* gate, re-added by the "Every role may fix main's failing check" decision 2026-10-25): the
|
|
203
|
+
* tests that already fail on main, and who owns them. The `ci` role owns the repair but does
|
|
204
|
+
* not monopolize it — any role whose change touches what a named test covers, or that can land
|
|
205
|
+
* a fix, fixes it in its own worktree and lets the change land normally. It is worded as
|
|
206
|
+
* permission to proceed (a main that names its failures stops nobody), never as "leave them
|
|
207
|
+
* alone". A pure function so its shape is pinned in tests; at most 20 tests are listed, then a
|
|
208
|
+
* `…and N more` line. */
|
|
184
209
|
export function buildMainKnownRedNote(failingTests) {
|
|
185
210
|
const shown = failingTests.slice(0, 20).map((t) => `- ${t}`);
|
|
186
211
|
if (failingTests.length > 20)
|
|
187
212
|
shown.push(`- …and ${failingTests.length - 20} more`);
|
|
188
213
|
return `<main-known-red>
|
|
189
214
|
${shown.join("\n")}
|
|
190
|
-
|
|
215
|
+
Main's tip already fails these tests. The ci role owns them and fixes them on its own clock. A role whose change touches what they test, or that can land a fix for one, fixes it in its own worktree and lets the change land normally — never weaken or delete a test to make the suite pass.
|
|
191
216
|
</main-known-red>`;
|
|
192
217
|
}
|
|
193
218
|
/** The note appended to an assigned series part's prompt naming the later planned parts that
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the previous note; when a note states something about the backlog (an entry is refused, a
|
|
4
4
|
* series is dead, a part is done) nothing used to check that claim again — on 2026-10-21 a plan
|
|
5
5
|
* run wrote "Codex 2b/9 carry Refused — never re-add or re-author them; the Codex theme is dead",
|
|
6
|
-
* then both Refused notes became Needs-
|
|
6
|
+
* then both Refused notes became Needs-review notes the plan role exists to act on, but its own
|
|
7
7
|
* notes kept telling it to skip them and the series stalled. This module renders, from the notes
|
|
8
8
|
* and the live backlog, a short block saying what is currently true of each entry and series the
|
|
9
9
|
* notes name, so the live state wins without anyone editing a notes file.
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* names that entry.
|
|
18
18
|
* Each named planned part carries its eligibility hold (entryHoldAgainst); a part in
|
|
19
19
|
* PLANS-DONE.md reads `done`, a part found nowhere reads `not in the backlog`. Each named series
|
|
20
|
-
* gets one summary line of its planned-part state. The block is at most 12 lines and
|
|
21
|
-
*
|
|
22
|
-
|
|
20
|
+
* gets one summary line of its planned-part state. The block is at most 12 lines and, when the
|
|
21
|
+
* notes name more items than fit, its last line says how many were dropped (`… and N more named
|
|
22
|
+
* items`) so a dropped item cannot read as a complete answer; it returns undefined when the notes
|
|
23
|
+
* name nothing live. */
|
|
24
|
+
import { entryHoldAgainst } from "../backlog/backlog-eligibility.js";
|
|
23
25
|
import { entryKey, partKey, plannedPartKeys, seriesPart } from "../backlog/backlog-parts.js";
|
|
26
|
+
import { plural } from "../text/phrases.js";
|
|
24
27
|
import { collapseWhitespace, escapeRegExp } from "../text/text.js";
|
|
25
28
|
const MAX_BLOCK_LINES = 12;
|
|
26
29
|
const HEADER = "What the backlog says now about entries your notes name (PLANS.md and BUGS.md as of this run; where your notes disagree, this is current):";
|
|
@@ -37,16 +40,14 @@ function seriesInfos(planned, doneTitles) {
|
|
|
37
40
|
const key = partKey(part.series, part.part);
|
|
38
41
|
let info = map.get(part.series.toLowerCase());
|
|
39
42
|
if (info === undefined) {
|
|
40
|
-
info = { name: part.series, of: part.of, plannedCount: 0,
|
|
43
|
+
info = { name: part.series, of: part.of, plannedCount: 0, review: 0, waiting: 0, refused: 0, holds: new Map(), done: new Set() };
|
|
41
44
|
map.set(part.series.toLowerCase(), info);
|
|
42
45
|
}
|
|
43
46
|
info.plannedCount++;
|
|
44
47
|
const hold = entryHoldAgainst(entry, allParts);
|
|
45
48
|
info.holds.set(key, hold);
|
|
46
|
-
if (hold === "needs-
|
|
47
|
-
info.
|
|
48
|
-
else if (hold === "needs-review" || hold === "held-for-operator")
|
|
49
|
-
info.review++; // vocabulary-ok: the held-for-operator hold is an operator decision
|
|
49
|
+
if (hold === "needs-review")
|
|
50
|
+
info.review++;
|
|
50
51
|
else if (hold === "refused")
|
|
51
52
|
info.refused++;
|
|
52
53
|
else if (typeof hold === "object" && hold !== null)
|
|
@@ -85,11 +86,8 @@ function partRefsInLine(line) {
|
|
|
85
86
|
function firstSixWords(normalizedKey) {
|
|
86
87
|
return normalizedKey.split(/\s+/).slice(0, 6).join(" ");
|
|
87
88
|
}
|
|
88
|
-
/** The wording of one named item's state: an eligibility hold, `done`, or `not in the backlog`.
|
|
89
|
-
|
|
90
|
-
* matching release command (`tumwater plan --release` for a plan, `tumwater bug --release` for a
|
|
91
|
-
* bug) — the same Plan/Bug derivation the operator-held alert uses. */
|
|
92
|
-
function holdWording(state, file = "plans") {
|
|
89
|
+
/** The wording of one named item's state: an eligibility hold, `done`, or `not in the backlog`. */
|
|
90
|
+
function holdWording(state) {
|
|
93
91
|
if (state === "done")
|
|
94
92
|
return "done";
|
|
95
93
|
if (state === "not-in-backlog")
|
|
@@ -98,27 +96,19 @@ function holdWording(state, file = "plans") {
|
|
|
98
96
|
return "eligible";
|
|
99
97
|
if (state === "refused")
|
|
100
98
|
return "refused (only the operator clears it)"; // vocabulary-ok: the plan-mandated refused hold wording refers to the tumwater operator
|
|
101
|
-
if (state === "needs-review")
|
|
102
|
-
return "needs review";
|
|
103
|
-
if (state === "needs-replan")
|
|
104
|
-
return "needs replan (the plan role rewrites it)";
|
|
105
|
-
if (state === "held-for-operator")
|
|
106
|
-
return `held for the operator (tumwater ${entryKindLabel(file).toLowerCase()} --release)`; // vocabulary-ok: names the operator-held wording and the operator's release command
|
|
107
|
-
if (state === "waiting-for-failure")
|
|
108
|
-
return "waiting for the next failure";
|
|
99
|
+
if (state === "needs-review" || state === "held-for-operator")
|
|
100
|
+
return "needs review"; // vocabulary-ok: the deprecated held-for-operator hold name, kept only until part 2f/6 retires it
|
|
109
101
|
return `waits on ${state.blockedBy.join(", ")}`;
|
|
110
102
|
}
|
|
111
103
|
/** The one summary line of a named series' planned-part state. */
|
|
112
104
|
function seriesSummary(info) {
|
|
113
105
|
const counts = [];
|
|
114
|
-
if (info.replan > 0)
|
|
115
|
-
counts.push(`${info.replan} ${info.replan === 1 ? "needs a replan" : "need a replan"}`);
|
|
116
106
|
if (info.review > 0)
|
|
117
107
|
counts.push(`${info.review} ${info.review === 1 ? "needs review" : "need review"}`);
|
|
118
108
|
if (info.waiting > 0)
|
|
119
109
|
counts.push(`${info.waiting} ${info.waiting === 1 ? "waits" : "wait"} on an earlier part`);
|
|
120
110
|
counts.push(info.refused === 0 ? "none refused" : `${info.refused} refused`);
|
|
121
|
-
return `${info.plannedCount
|
|
111
|
+
return `${plural(info.plannedCount, "part")} planned: ${counts.join(", ")}`;
|
|
122
112
|
}
|
|
123
113
|
/** The live backlog block for the notes, or undefined when they name nothing live. */
|
|
124
114
|
export function notesBacklogBlock(notes, backlog) {
|
|
@@ -173,12 +163,15 @@ export function notesBacklogBlock(notes, backlog) {
|
|
|
173
163
|
const info = series.get(p.seriesLower);
|
|
174
164
|
if (info === undefined)
|
|
175
165
|
continue;
|
|
176
|
-
lines.push(`- ${info.name}, part ${p.part}/${p.of} — ${holdWording(p.state
|
|
166
|
+
lines.push(`- ${info.name}, part ${p.part}/${p.of} — ${holdWording(p.state)}`);
|
|
177
167
|
}
|
|
178
168
|
for (const entry of namedEntries.values())
|
|
179
|
-
lines.push(`- ${entry.title} — ${holdWording(entry.state
|
|
169
|
+
lines.push(`- ${entry.title} — ${holdWording(entry.state)}`);
|
|
180
170
|
if (lines.length === 0)
|
|
181
171
|
return undefined;
|
|
182
|
-
const
|
|
183
|
-
|
|
172
|
+
const cap = MAX_BLOCK_LINES - 1; // the item lines; the header is the twelfth line
|
|
173
|
+
if (lines.length <= cap)
|
|
174
|
+
return [HEADER, ...lines].join("\n");
|
|
175
|
+
const shown = lines.slice(0, cap - 1);
|
|
176
|
+
return [HEADER, ...shown, `… and ${plural(lines.length - shown.length, "more named item")}`].join("\n");
|
|
184
177
|
}
|
|
@@ -12,3 +12,14 @@ export function readPrinciples(root) {
|
|
|
12
12
|
const file = path.join(root, "PRINCIPLES.md");
|
|
13
13
|
return truncateWithNote(readTextOrNull(file)?.trim() ?? "", PRINCIPLES_MAX_CHARS, "PRINCIPLES.md");
|
|
14
14
|
}
|
|
15
|
+
/** True when PRINCIPLES.md's trimmed text exceeds the prompt cap, so every run's <principles>
|
|
16
|
+
* block carries only its head plus a truncation marker — the always-on amber twin of the doctor
|
|
17
|
+
* `principles` row's over-cap warn, surfaced on `tumwater status`, the dashboard and the TUI
|
|
18
|
+
* without a doctor run. Uses the doctor row's own predicate (raw.trim().length >
|
|
19
|
+
* PRINCIPLES_MAX_CHARS), so the alert and the doctor row cannot disagree. Missing, unreadable
|
|
20
|
+
* (the directory-in-place case readPrinciples degrades to "") and at-or-under-cap files are all
|
|
21
|
+
* false. */
|
|
22
|
+
export function principlesBlockOverCap(root) {
|
|
23
|
+
const raw = readTextOrNull(path.join(root, "PRINCIPLES.md"));
|
|
24
|
+
return raw !== null && raw.trim().length > PRINCIPLES_MAX_CHARS;
|
|
25
|
+
}
|
|
@@ -68,24 +68,16 @@ ${REPLY_ENDINGS}`;
|
|
|
68
68
|
* so the bridge must send the new run to `git diff` before it starts its own work. A `salvaged`
|
|
69
69
|
* handoff's edits were restored from a patch of a run the harness lost track of
|
|
70
70
|
* (src/git/slot-salvage.ts) rather than kept in place. */
|
|
71
|
-
export function buildHandoffPrompt(roleId, runPrompt, cause = "handoff"
|
|
72
|
-
/** Named facts an image-reroute handoff note needs that the cause alone cannot carry: the
|
|
73
|
-
* model the blind run was on and the image file it read. */
|
|
74
|
-
imageReroute) {
|
|
71
|
+
export function buildHandoffPrompt(roleId, runPrompt, cause = "handoff") {
|
|
75
72
|
const note = cause === "budget-capped"
|
|
76
73
|
? `An earlier API-billed claude run of the "${roleId}" role hit its per-run budget cap before it
|
|
77
74
|
could finish; its uncommitted edits are in your worktree — review \`git diff\`, then finish or
|
|
78
75
|
discard them. This run starts on the model job the daily budget left you.`
|
|
79
|
-
: cause === "
|
|
80
|
-
? `An earlier run of the "${roleId}" role
|
|
81
|
-
when it read ${imageReroute?.path ?? "an image"}, because that model cannot read images; its
|
|
82
|
-
uncommitted edits are in your worktree — review \`git diff\`, then finish or discard them. This
|
|
83
|
-
run is on a model that CAN read images, so read the image again.`
|
|
84
|
-
: cause === "salvaged"
|
|
85
|
-
? `An earlier run of the "${roleId}" role was cut short before tumwater could save its work;
|
|
76
|
+
: cause === "salvaged"
|
|
77
|
+
? `An earlier run of the "${roleId}" role was cut short before tumwater could save its work;
|
|
86
78
|
its uncommitted edits were recovered and are in your worktree — review \`git status\` and
|
|
87
79
|
\`git diff\` (new files show as untracked), then finish or discard them.`
|
|
88
|
-
|
|
80
|
+
: `An earlier run of the "${roleId}" role on another model was interrupted; its
|
|
89
81
|
uncommitted edits are in your worktree — review \`git diff\`, then finish or discard them.`;
|
|
90
82
|
return `${runPrompt}\n\n${note}`;
|
|
91
83
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { describeCheck } from "../build/build-check-report.js";
|
|
3
|
-
import { DECOMPOSITION_GUIDANCE,
|
|
3
|
+
import { DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING } from "../roles/role-guidance.js";
|
|
4
4
|
import { CLAIMS_RULE, REPLY_ENDINGS } from "../verdict/reply-contract.js";
|
|
5
5
|
import { todayStamp } from "../budget/budget.js";
|
|
6
6
|
import { daysBetween } from "../text/datetime.js";
|
|
@@ -93,7 +93,7 @@ function backendRuleLines(rules) {
|
|
|
93
93
|
/** The date line every pi prompt carries — run and director (via sharedPreamble) and the gate's
|
|
94
94
|
* conflict and review runs — naming the local calendar day as YYYY-MM-DD. No prompt
|
|
95
95
|
* used to say what day it is, so a run that had to write one (a BUGS.md heading's "(found by …
|
|
96
|
-
* YYYY-MM-DD)", a Fixed or Refused date, NEEDS_REVIEW_NOTE's
|
|
96
|
+
* YYYY-MM-DD)", a Fixed or Refused date, NEEDS_REVIEW_NOTE's
|
|
97
97
|
* <YYYY-MM-DD>, a plan deadline)
|
|
98
98
|
* inferred it from the newest dates in the repo, which were themselves drifting: the team
|
|
99
99
|
* stamped entries days into the future and each wrong date seeded the next. The day is
|
|
@@ -261,7 +261,7 @@ note's say-so.`
|
|
|
261
261
|
source of what is currently true of each entry and series, so check the entry itself (and, when this run carries
|
|
262
262
|
one, its line in the backlog) before acting on a note's say-so.`;
|
|
263
263
|
return `Notes your role wrote in earlier runs (${savedClause(savedDay, today)}yours, unverified — check against the code before
|
|
264
|
-
relying on them). A claim they make about an entry's backlog state — refused, blocked, abandoned,
|
|
264
|
+
relying on them). A claim they make about an entry's backlog state — refused, blocked, abandoned,
|
|
265
265
|
done, behind another entry — is a snapshot of the past, not the present: ${liveSource}
|
|
266
266
|
A note that tells you to skip or never re-author an entry is always overridden by the backlog.
|
|
267
267
|
<role-notes>\n${notes.trim()}\n</role-notes>`;
|
|
@@ -307,7 +307,7 @@ from the repo.`,
|
|
|
307
307
|
}
|
|
308
308
|
/** The prompt for one role run. */
|
|
309
309
|
export function buildRunPrompt(input) {
|
|
310
|
-
const { role, rejectedReview, principles, digest, coverage, ciParity, filings, backlogStructure, backlogIndex, notes, notesSavedDay, notesBacklog, extraInstructions, planTargetNote, flakyChecks, check, gateCommand, briefFile, today, userRequest, agent, assigned } = input;
|
|
310
|
+
const { role, rejectedReview, principles, digest, coverage, ciParity, filings, backlogStructure, backlogIndex, notes, notesSavedDay, notesBacklog, extraInstructions, planTargetNote, flakyChecks, noCheck, check, gateCommand, briefFile, today, userRequest, agent, assigned } = input;
|
|
311
311
|
const dialect = dialectFor(agent);
|
|
312
312
|
const day = today ?? todayStamp();
|
|
313
313
|
const parts = [
|
|
@@ -338,6 +338,8 @@ role's scope, say so in your reply instead of doing it anyway.\n<user-request>\n
|
|
|
338
338
|
if (notesBacklog)
|
|
339
339
|
parts.push(notesBacklog);
|
|
340
340
|
parts.push(`Your task this run:\n${role.find.trim()}`);
|
|
341
|
+
if (noCheck)
|
|
342
|
+
parts.push(noCheck.trim());
|
|
341
343
|
if (planTargetNote)
|
|
342
344
|
parts.push(planTargetNote.trim());
|
|
343
345
|
if (flakyChecks)
|
|
@@ -396,8 +398,7 @@ work yourself:
|
|
|
396
398
|
direction — so roles can pick it up again.
|
|
397
399
|
- A decision about a marked plan (e.g. "split plan X", "keep plan X whole", "replan X"):
|
|
398
400
|
split it into independently landable sub-plans per PLAN_SIZING, or clear the
|
|
399
|
-
${NEEDS_REVIEW_NOTE} note
|
|
400
|
-
the user's direction.
|
|
401
|
+
${NEEDS_REVIEW_NOTE} note — per the user's direction.
|
|
401
402
|
- A request to manage custom roles ("add a role named X that does Y", "remove X",
|
|
402
403
|
"move X before Y"): write the FULL replacement customRoles array to
|
|
403
404
|
.tumwater-config-request.json at your worktree root — the shape is
|
|
@@ -421,12 +422,14 @@ work yourself:
|
|
|
421
422
|
parts.push(`Additional standing instructions from the user:\n${extraInstructions.trim()}`);
|
|
422
423
|
// Before the shared rules, not after them: the closing reply contract stays the last thing
|
|
423
424
|
// in the prompt, where a model attends to it most (the run prompt's invariant too).
|
|
424
|
-
parts.push(`Note on one boundary in the rules below, director only: user-defined-role requests
|
|
425
|
-
executed by writing .tumwater-config-request.json in your worktree
|
|
426
|
-
above) — never by editing tumwater.json, which stays off-limits to you
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
425
|
+
parts.push(`Note on one boundary in the rules below, director only: user-defined-role requests and a
|
|
426
|
+
project check declaration are executed by writing .tumwater-config-request.json in your worktree
|
|
427
|
+
(shape and worked example above) — never by editing tumwater.json, which stays off-limits to you
|
|
428
|
+
as to every role. The request may carry only customRoles and check.command: tumwater validates it
|
|
429
|
+
and runs a declared check.command green in the worktree before writing it. A request that also
|
|
430
|
+
names any other setting (timeouts, budgets, role enablement, review settings, gateCommand,
|
|
431
|
+
mergeCommand, check.cwd) has that key discarded with a warning, so such a request is guidance
|
|
432
|
+
that stays staged or is recorded per the routing rules, not an edit you
|
|
430
433
|
can make.`);
|
|
431
434
|
// A director run is never assigned an entry.
|
|
432
435
|
parts.push(commonRules(check, briefFile, undefined, dialectFor(backend)).trim());
|
|
@@ -20,7 +20,10 @@ export function track(promise) {
|
|
|
20
20
|
* shape prewarms nothing further — there is no point staging a compile for a tree the check
|
|
21
21
|
* condemned — and is simply not adopted (the episode re-runs it under its own rules).
|
|
22
22
|
* Skipped while an episode is pending: its own steps are the warm-up, and the mirror worktree
|
|
23
|
-
* both run in must not serve two checks at once.
|
|
23
|
+
* both run in must not serve two checks at once. A new head that arrives while a step is still
|
|
24
|
+
* running is likewise not adopted: returning without resetting keeps the mirror pointed at the
|
|
25
|
+
* head the running check (or compile) is testing, never re-pointed under it; the next poll after
|
|
26
|
+
* both settle takes the newest head as today. */
|
|
24
27
|
export class PrewarmProbes {
|
|
25
28
|
deps;
|
|
26
29
|
head = null;
|
|
@@ -37,6 +40,13 @@ export class PrewarmProbes {
|
|
|
37
40
|
if (pending)
|
|
38
41
|
return;
|
|
39
42
|
if (this.head !== head) {
|
|
43
|
+
// A newer head arrived while this head's green check or staged compile is still running
|
|
44
|
+
// (!done): never re-point the mirror under a running step — it may still be testing the
|
|
45
|
+
// sha it read before the reset, and a verdict recorded under that sha would not cover the
|
|
46
|
+
// tree it tested. Keep probing for the older head; the next poll after both settle adopts
|
|
47
|
+
// the newest head as today.
|
|
48
|
+
if ((this.green !== null && !this.green.done) || (this.compiled !== null && !this.compiled.done))
|
|
49
|
+
return;
|
|
40
50
|
this.head = head;
|
|
41
51
|
this.green = null;
|
|
42
52
|
this.compiled = null;
|
|
@@ -254,6 +254,32 @@ export class Redeployer {
|
|
|
254
254
|
this.autoRestartOn = autoRestart;
|
|
255
255
|
if (!this.selfHosted || !mainHead)
|
|
256
256
|
return this.endDrain();
|
|
257
|
+
await this.observeHead(mainHead, now);
|
|
258
|
+
// The sustained-pin escalation runs BEFORE the early returns below: a latched block on a
|
|
259
|
+
// frozen main never re-enters the episode logic, and that pinned-and-stuck state is exactly
|
|
260
|
+
// what must escalate (BUGS.md 2026-09-29).
|
|
261
|
+
if (this.staleness?.stale && autoRestart)
|
|
262
|
+
this.escalateIfSustained(now);
|
|
263
|
+
if (!this.staleness?.stale || !autoRestart || this.blockedHead === mainHead)
|
|
264
|
+
return this.endDrain();
|
|
265
|
+
const cooldown = this.cooldownStage(mainHead, now);
|
|
266
|
+
if (cooldown !== null)
|
|
267
|
+
return cooldown;
|
|
268
|
+
const start = await this.startEpisode(mainHead, inFlight, now);
|
|
269
|
+
if (start !== null)
|
|
270
|
+
return start;
|
|
271
|
+
const green = this.greenStage(mainHead, now);
|
|
272
|
+
if (green !== null)
|
|
273
|
+
return green;
|
|
274
|
+
const compile = this.compileStage(mainHead, now);
|
|
275
|
+
if (compile !== null)
|
|
276
|
+
return compile;
|
|
277
|
+
return this.drainAndSwap(mainHead, inFlight, now);
|
|
278
|
+
}
|
|
279
|
+
/** Observe a moved main head (a no-op on an unchanged head): update staleness, log build_stale
|
|
280
|
+
* on the fresh transition, run the sustained-pin clock and its forced-restart waiver, and drop
|
|
281
|
+
* any restart pending for a superseded head. */
|
|
282
|
+
async observeHead(mainHead, now) {
|
|
257
283
|
if (mainHead !== this.lastHead) {
|
|
258
284
|
const wasStale = this.staleness?.stale ?? false;
|
|
259
285
|
this.lastHead = mainHead;
|
|
@@ -286,13 +312,12 @@ export class Redeployer {
|
|
|
286
312
|
if (this.pendingHead !== null && this.pendingHead !== mainHead)
|
|
287
313
|
this.clearPending();
|
|
288
314
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return this.endDrain();
|
|
315
|
+
}
|
|
316
|
+
/** The restart cooldown / forced-restart waiver gate: while the deferral holds, warn once, keep
|
|
317
|
+
* the team running on the stale build, prewarm the deferred head's green check and compile, and
|
|
318
|
+
* end any drain. Once lapsed, say why a red running build starts the episode early. Returns
|
|
319
|
+
* null when the cooldown has passed and the episode may begin. */
|
|
320
|
+
cooldownStage(mainHead, now) {
|
|
296
321
|
// Completed auto-restarts are rate-limited to one per RESTART_COOLDOWN_MS (BUGS.md 2026-09-11):
|
|
297
322
|
// under sustained churn every stale head would otherwise drive a full hold+drain+swap episode
|
|
298
323
|
// back to back. Inside the cooldown the team keeps running on the stale build exactly as when
|
|
@@ -341,6 +366,12 @@ export class Redeployer {
|
|
|
341
366
|
this.warn(`auto-restart of ${shortSha(mainHead)} proceeding early — the running version ${shortSha(this.build.sha)} fails its check, ` +
|
|
342
367
|
`the cooldown was cut to ${RESTART_URGENT_COOLDOWN_MIN} min and that deadline has already passed`);
|
|
343
368
|
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
/** The boot gate behind which an episode opens: refuse to hold onto a generation that cannot
|
|
372
|
+
* boot here, else adopt the pre-warmed green/compile verdicts and hold the team until the
|
|
373
|
+
* staged tree is ready. Returns null when an episode is already pending. */
|
|
374
|
+
async startEpisode(mainHead, inFlight, now) {
|
|
344
375
|
if (this.pendingHead === null) {
|
|
345
376
|
// Before holding anything: could a new generation even boot here? A refusal here costs
|
|
346
377
|
// nothing — no hold, no green check, no compile — so asking again every poll is how a
|
|
@@ -369,6 +400,11 @@ export class Redeployer {
|
|
|
369
400
|
this.logRestartPending(mainHead);
|
|
370
401
|
return "hold";
|
|
371
402
|
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/** The green check: hold while it is unresolved, drop the episode when it could not run, block
|
|
406
|
+
* when it returns a red verdict. Returns null (proceed to compile) only on a green verdict. */
|
|
407
|
+
greenStage(mainHead, now) {
|
|
372
408
|
if (!this.green?.done)
|
|
373
409
|
return "hold";
|
|
374
410
|
// A REJECTED check is not a verdict: it could not run (git broke inside the mirror worktree,
|
|
@@ -390,6 +426,12 @@ export class Redeployer {
|
|
|
390
426
|
this.block(mainHead, reason, `${reason} — holding the restart until main is green`);
|
|
391
427
|
return this.endDrain();
|
|
392
428
|
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
/** The compile stage: kick off (or hold on) the staged build, and on a verdict block or drop the
|
|
432
|
+
* episode when the tree failed to build. Returns null (proceed to drain) only on a successful
|
|
433
|
+
* compile. */
|
|
434
|
+
compileStage(mainHead, now) {
|
|
393
435
|
if (!this.compiled) {
|
|
394
436
|
this.compiled = this.probes.adoptCompile(mainHead) ?? track(this.deps.compile(mainHead));
|
|
395
437
|
this.logRestartPending(mainHead);
|
|
@@ -425,6 +467,11 @@ export class Redeployer {
|
|
|
425
467
|
this.block(mainHead, reason, `${reason} — staying on version ${shortSha(this.build.sha)}: ${detail}`);
|
|
426
468
|
return this.endDrain();
|
|
427
469
|
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
/** The drain and swap: hold while runs keep the drain open, re-ask the boot gate before the
|
|
473
|
+
* point of no return, then swap the staged tree in and record the completed restart. */
|
|
474
|
+
async drainAndSwap(mainHead, inFlight, now) {
|
|
428
475
|
// The drain rule, split by who requested the work (BUGS.md 2026-09-08): a director run is
|
|
429
476
|
// an explicit human prompt and outranks the redeploy — while one is in flight the hold has no
|
|
430
477
|
// time cap: no swap and no abort until it finishes. The per-run watchdogs already bound how
|
|
@@ -5,6 +5,7 @@ import { RESTART_EXIT_CODE } from "./redeploy-policy.js";
|
|
|
5
5
|
import { childEnv } from "../process/process.js";
|
|
6
6
|
import { startParentDeathWatch } from "../process/supervisor.js";
|
|
7
7
|
import { liveNodeBin } from "../process/node-bin.js";
|
|
8
|
+
import { parsePositiveInt } from "../text/text.js";
|
|
8
9
|
/** Auto-reload for the user-launched dashboards (`tumwater tui`, `tumwater gui`).
|
|
9
10
|
*
|
|
10
11
|
* Unlike the orchestrator — which the supervisor respawns onto a fresh build — a dashboard is
|
|
@@ -50,12 +51,16 @@ export function watchReloadSupervisor(onGone, seams = {}) {
|
|
|
50
51
|
const mark = (seams.env ?? process.env)[DASHBOARD_CHILD_ENV];
|
|
51
52
|
if (mark === undefined)
|
|
52
53
|
return () => { };
|
|
53
|
-
|
|
54
|
+
// The mark is a pid spelled in plain decimal (reexecSelf writes String(process.pid)); a
|
|
55
|
+
// hex/exponent/whitespace spelling is never a pid the writer meant, so read it by the shared
|
|
56
|
+
// plain-decimal rule rather than Number()'s silent coercion — the supervisor's expecter twin
|
|
57
|
+
// parses the same way.
|
|
58
|
+
const supervisor = parsePositiveInt(mark);
|
|
54
59
|
return startParentDeathWatch(onGone, {
|
|
55
60
|
...(seams.ppid ? { ppid: seams.ppid } : {}),
|
|
56
61
|
intervalMs: seams.intervalMs ?? SUPERVISOR_POLL_MS,
|
|
57
62
|
// An older supervisor's "1" names no pid (1 is launchd/init): the watch's first read stands.
|
|
58
|
-
...(
|
|
63
|
+
...(supervisor !== null && supervisor > 1 ? { expectedPpid: supervisor } : {}),
|
|
59
64
|
});
|
|
60
65
|
}
|
|
61
66
|
/** Hand this process's dashboard duty to a fresh copy of itself (`process.argv.slice(1)` re-runs
|