tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* probe's runMarkers check and doctor's orphan judgment), and the sweep that SIGTERMs
|
|
5
5
|
* this run's leftovers whatever process group they ended up in. Split out of
|
|
6
6
|
* process-table.ts — which keeps the doctor-facing probe interface and the ps/lsof/top
|
|
7
|
-
* parsing it is made of — because this half serves src/agent/
|
|
7
|
+
* parsing it is made of — because this half serves src/agent/agent-spawn.ts's run stamping and
|
|
8
8
|
* the exit sweep, and shares none of the probe's table-parsing concerns; the probe reads marks
|
|
9
9
|
* through the same parsers exported here. */
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
11
|
import fs from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import { plural } from "../text/phrases.js";
|
|
14
|
-
import { execFileAsync, withoutLaunchServicesCheckIn } from "./process.js";
|
|
13
|
+
import { agree, plural } from "../text/phrases.js";
|
|
14
|
+
import { execFileAsync, KILL_GRACE_MS, withoutLaunchServicesCheckIn } from "./process.js";
|
|
15
15
|
/** Every lookup normally finishes in well under a second; a wedged one must not hang
|
|
16
16
|
* its caller — the same ceiling process-table.ts's probe lookups carry. */
|
|
17
17
|
const PS_ENVIRON_TIMEOUT_MS = 10_000;
|
|
@@ -22,14 +22,33 @@ const PS_ENVIRON_TIMEOUT_MS = 10_000;
|
|
|
22
22
|
* command `detached`, a group leader in its own right, so a backgrounded grandchild sits in
|
|
23
23
|
* a group whose leader is not pi and the group sweep's kill(-pi.pid) never reaches it. */
|
|
24
24
|
const RUN_MARKER_VAR = "TUMWATER_RUN";
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
/** The first 8 hex of sha1 of `root`'s realpath — the short stable token that names a team
|
|
26
|
+
* root inside a run marker's third segment, so a dead-marked orphan can be attributed to the
|
|
27
|
+
* checkout that minted it (BUGS.md, "Reap the doctor's orphans, part 1/3"). Realpath-resolving
|
|
28
|
+
* first keeps two spellings of one root (e.g. macOS's /var vs /private/var) hashing identically;
|
|
29
|
+
* an unreadable root realpaths to its resolved given path. */
|
|
30
|
+
export function rootToken(root) {
|
|
31
|
+
const resolved = path.resolve(root);
|
|
32
|
+
let real;
|
|
33
|
+
try {
|
|
34
|
+
real = fs.realpathSync(resolved);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
real = resolved;
|
|
38
|
+
}
|
|
39
|
+
return crypto.createHash("sha1").update(real).digest("hex").slice(0, 8);
|
|
40
|
+
}
|
|
41
|
+
/** A fresh marker for one pi run: `<harness pid>-<random hex>` (`<rootToken>` appended when a
|
|
42
|
+
* team `root` is given). The pid prefix is what a reader of a leaked process's environment needs
|
|
43
|
+
* to tell a live run's straggler (its own sweep will reap it at exit) from a dead run's orphan
|
|
44
|
+
* (nothing will): a future doctor check can report the latter wherever the leak's cwd is. The
|
|
45
|
+
* random half makes the marker unique per run, so one run's sweep can never signal a concurrent
|
|
46
|
+
* sibling run's processes — a machine running the team has several pi runs alive at once. The
|
|
47
|
+
* optional root token stamps the team root the run belongs to, so a reader that sees a dead
|
|
48
|
+
* mark can tell this checkout's orphan from another checkout's and never signal the latter. */
|
|
49
|
+
export function makeRunMarker(pid = process.pid, root) {
|
|
50
|
+
const base = `${pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
51
|
+
return root === undefined ? base : `${base}-${rootToken(root)}`;
|
|
33
52
|
}
|
|
34
53
|
/** `base` plus this run's marker APPENDED to any inherited one, comma-separated. A harness
|
|
35
54
|
* run under test inside a pi run inherits the outer run's mark; keeping it means the inner
|
|
@@ -43,7 +62,7 @@ export function runMarkerEnv(base, marker) {
|
|
|
43
62
|
* check-in preload removed (`withoutLaunchServicesCheckIn`) and this run's marker appended
|
|
44
63
|
* (`runMarkerEnv`). All three adapters (pi, claude, opencode) layer their own settings on
|
|
45
64
|
* top, so the composition lives here rather than being re-derived in each. `binDir`, when
|
|
46
|
-
* given, is prepended to PATH —
|
|
65
|
+
* given, is prepended to PATH — agent-spawn passes the notes shim dir (notesShimDir) so every
|
|
47
66
|
* run finds the `tumwater-notes` command first (plans/notes-command.md "Reaching it from a
|
|
48
67
|
* run"). */
|
|
49
68
|
export function backendBaseEnv(marker, base = process.env, binDir) {
|
|
@@ -188,8 +207,7 @@ async function findMarkedPids(marker) {
|
|
|
188
207
|
export function leftoversSweptMessage(kind, count, killed) {
|
|
189
208
|
if (count === 0 || killed)
|
|
190
209
|
return undefined;
|
|
191
|
-
|
|
192
|
-
return `run sweep: ${plural(count, "process", "processes")} the ${kind} run started ${verb} still running after pi exited; sent SIGTERM`;
|
|
210
|
+
return `run sweep: ${plural(count, "process", "processes")} the ${kind} run started ${agree(count, "was", "were")} still running after pi exited; sent SIGTERM`;
|
|
193
211
|
}
|
|
194
212
|
/** SIGTERM every same-user process whose environment names `marker`, escalating to SIGKILL
|
|
195
213
|
* after 10 s for any that survive — the run's cross-group leftovers: what a tool call
|
|
@@ -226,7 +244,7 @@ export async function sweepRunMarker(marker) {
|
|
|
226
244
|
// Already gone.
|
|
227
245
|
}
|
|
228
246
|
}
|
|
229
|
-
},
|
|
247
|
+
}, KILL_GRACE_MS).unref();
|
|
230
248
|
}
|
|
231
249
|
return signaled;
|
|
232
250
|
}
|
|
@@ -113,37 +113,55 @@ export const MAX_RAPID_RESPAWNS = 5;
|
|
|
113
113
|
function exitCodeOf(exit) {
|
|
114
114
|
return exit.code ?? 1;
|
|
115
115
|
}
|
|
116
|
+
/** The code supervision must exit with when this generation ends it, or null when the
|
|
117
|
+
* generation asked for a restart and the loop should respawn it. A restart request while the
|
|
118
|
+
* operator is stopping is an asked-for ending, so it maps to the child's own code. */
|
|
119
|
+
function terminalExitCode(exit, stopping) {
|
|
120
|
+
if (exit.code === RESTART_EXIT_CODE && !stopping)
|
|
121
|
+
return null;
|
|
122
|
+
return exitCodeOf(exit);
|
|
123
|
+
}
|
|
124
|
+
/** Whether a terminal exit is a failure the operator did not ask for and so must be recorded
|
|
125
|
+
* through onTeamDown: not a clean exit, not a `run --once` failed round, and not while stopping. */
|
|
126
|
+
function isUnaskedFailure(code, stopping) {
|
|
127
|
+
return code !== 0 && code !== ONCE_FAILED_EXIT_CODE && !stopping;
|
|
128
|
+
}
|
|
129
|
+
/** Record one respawn at `at`, drop the ones that aged out of RESPAWN_WINDOW_MS, and return
|
|
130
|
+
* whether the rapid-respawn guard is now tripped. Mutates `recentRespawns`, the loop's window. */
|
|
131
|
+
function trackRespawn(recentRespawns, at) {
|
|
132
|
+
while (recentRespawns.length > 0 && at - (recentRespawns[0] ?? at) > RESPAWN_WINDOW_MS)
|
|
133
|
+
recentRespawns.shift();
|
|
134
|
+
recentRespawns.push(at);
|
|
135
|
+
return recentRespawns.length > MAX_RAPID_RESPAWNS;
|
|
136
|
+
}
|
|
137
|
+
/** Run a best-effort hook: a throwing hook must not change the supervisor's exit, so its error
|
|
138
|
+
* is swallowed. Used for onTeamDown (the trace) and onDone (the build repair). */
|
|
139
|
+
async function runBestEffort(hook) {
|
|
140
|
+
try {
|
|
141
|
+
await hook?.();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Best-effort: the hook's own reporting is its responsibility.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
116
147
|
/** Run child generations until one exits with something other than a restart request, and
|
|
117
148
|
* return the code the supervisor should exit with. Pure apart from the injected effects. */
|
|
118
149
|
export async function superviseStart(opts, signal) {
|
|
119
150
|
const now = opts.now ?? Date.now;
|
|
120
151
|
let generation = 1;
|
|
121
152
|
const recentRespawns = [];
|
|
122
|
-
const teamDown =
|
|
123
|
-
try {
|
|
124
|
-
await opts.onTeamDown?.(down);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// Best-effort (see onTeamDown): a trace that cannot be written must not change the exit.
|
|
128
|
-
}
|
|
129
|
-
};
|
|
153
|
+
const teamDown = (down) => runBestEffort(() => opts.onTeamDown?.(down));
|
|
130
154
|
try {
|
|
131
155
|
for (;;) {
|
|
132
156
|
const exit = await opts.spawnChild(signal);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const code = exitCodeOf(exit);
|
|
138
|
-
if (code !== 0 && code !== ONCE_FAILED_EXIT_CODE && !opts.stopping())
|
|
157
|
+
const stopping = opts.stopping();
|
|
158
|
+
const terminal = terminalExitCode(exit, stopping);
|
|
159
|
+
if (terminal !== null) {
|
|
160
|
+
if (isUnaskedFailure(terminal, stopping))
|
|
139
161
|
await teamDown({ generation, exit, crashLoop: false });
|
|
140
|
-
return
|
|
162
|
+
return terminal;
|
|
141
163
|
}
|
|
142
|
-
|
|
143
|
-
while (recentRespawns.length > 0 && t - (recentRespawns[0] ?? t) > RESPAWN_WINDOW_MS)
|
|
144
|
-
recentRespawns.shift();
|
|
145
|
-
recentRespawns.push(t);
|
|
146
|
-
if (recentRespawns.length > MAX_RAPID_RESPAWNS) {
|
|
164
|
+
if (trackRespawn(recentRespawns, now())) {
|
|
147
165
|
opts.onCrashLoop?.();
|
|
148
166
|
await teamDown({ generation, exit, crashLoop: true });
|
|
149
167
|
return 1;
|
|
@@ -153,17 +171,11 @@ export async function superviseStart(opts, signal) {
|
|
|
153
171
|
}
|
|
154
172
|
}
|
|
155
173
|
finally {
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
await opts.onDone?.();
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
// Best-effort — see onDone.
|
|
162
|
-
}
|
|
174
|
+
await runBestEffort(() => opts.onDone?.());
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
/** The `supervisor_exit` event for a team that went down (see onTeamDown). `diagnosis` is the
|
|
166
|
-
* startup gate re-asked after the death (startup-
|
|
178
|
+
* startup gate re-asked after the death (startup-gates.ts's runStartupProblem): the likeliest
|
|
167
179
|
* reason a generation dies right after a respawn is an environment that no longer boots, and the
|
|
168
180
|
* child's own message reached only the terminal. Null when every precondition passes — the event
|
|
169
181
|
* then carries no reason rather than a guess. A crash loop's reason is the guard itself. */
|
|
@@ -56,7 +56,7 @@ export const CODEX_DIALECT = {
|
|
|
56
56
|
],
|
|
57
57
|
};
|
|
58
58
|
/** The dialect table keyed by backend id: every backend but claude speaks pi's dialect (and
|
|
59
|
-
* an unknown id falls back to pi because run
|
|
59
|
+
* an unknown id falls back to pi because agent-run.ts routes an unregistered id to the pi
|
|
60
60
|
* backend). Adding a backend means one row here, not another branch. */
|
|
61
61
|
const DIALECTS = {
|
|
62
62
|
pi: PI_DIALECT,
|
|
@@ -51,7 +51,7 @@ export function buildRejectedReviewNote(review, role) {
|
|
|
51
51
|
return `Your previous change was rejected; fix each numbered objection or drop the change — it is this run's primary task${context}:\n${list}${tail}`;
|
|
52
52
|
}
|
|
53
53
|
/** The note injected into a role's run prompt when its rejected change has been re-applied to
|
|
54
|
-
* current main as uncommitted edits (plans/revise-rejected.md, runner/revision.ts's
|
|
54
|
+
* current main as uncommitted edits (plans/revise-rejected.md, runner/runner-revision.ts's
|
|
55
55
|
* applyRevision): this run's one task is to revise the change already in the worktree, not
|
|
56
56
|
* author a new one. The prior objections are numbered so the author can work through them; a
|
|
57
57
|
* revision round past the first also reaches here. The chain keeps revising while each round
|
|
@@ -156,7 +156,7 @@ ${lines.join("\n")}
|
|
|
156
156
|
</merged-prerequisites>`;
|
|
157
157
|
}
|
|
158
158
|
/** The note injected into a role's prompt after leftover recovery discarded its pinned change
|
|
159
|
-
* at MERGE_CONFLICT_LIMIT (src/runner/leftover.ts): at the cap recovery ordinarily hands the
|
|
159
|
+
* at MERGE_CONFLICT_LIMIT (src/runner/runner-leftover.ts): at the cap recovery ordinarily hands the
|
|
160
160
|
* change back to its author with markers instead, so a discard means that lineage already used
|
|
161
161
|
* its hand-back. The merge failures were harness-level, so without the note the author would
|
|
162
162
|
* never learn its work is gone — or that redoing it from memory of the old diff would conflict
|
|
@@ -156,7 +156,7 @@ ${TEST_RUNNER_RULE}${noCheckRunRule}
|
|
|
156
156
|
* revision of a previously rejected change (plans/revise-rejected.md part 2/2): it names the round
|
|
157
157
|
* and shows the prior objections plus the interdiff so the re-review checks each one first.
|
|
158
158
|
* Omitted, the prompt is unchanged from a fresh change's. `postMerge` marks the post-merge review
|
|
159
|
-
* of a commit already on main
|
|
159
|
+
* of a commit already on main: a finding the reviewer can repair is the work —
|
|
160
160
|
* it makes the minimal fix in the worktree, leaves the edit, and ends with the verdict line plus a
|
|
161
161
|
* `FIX:` line — so the instruction and the repair rule render only for that caller, and every other
|
|
162
162
|
* (pre-merge) caller's prompt is byte-identical. */
|
|
@@ -286,49 +286,6 @@ ${TEST_RUNNER_RULE}
|
|
|
286
286
|
FIX: <files you changed>` : ""}`);
|
|
287
287
|
return parts.join("\n\n");
|
|
288
288
|
}
|
|
289
|
-
/** The prompt for the next attempt at a review whose previous run hit its time budget while
|
|
290
|
-
* still making progress (RunnerState.reviewResume): that run continues the timed-out reviewer's
|
|
291
|
-
* own session (`--continue`), so the diff and everything the reviewer already read are in
|
|
292
|
-
* context. This prompt carries no diff — re-sending it would defeat the resume — and names the
|
|
293
|
-
* frozen comparison base and the unchanged patch-id so the reviewer knows the change it was
|
|
294
|
-
* judging has not moved. `baseRev` is changeBaseRev's frozen revision (see buildReviewPrompt);
|
|
295
|
-
* `patchId` is git/git.ts's patchId for the change. It closes with the same VERDICT_ENDING
|
|
296
|
-
* contract as the full review prompt, so the accepted forms cannot drift. */
|
|
297
|
-
export function buildReviewResumePrompt(baseRev, patchId) {
|
|
298
|
-
return `${dateLine()}
|
|
299
|
-
|
|
300
|
-
You are resuming an adversarial review of a tumwater change: the previous attempt hit its time budget while still making progress, and this run continues that same reviewer session. Everything you already read — the diff and the files you checked — is in your context; do not re-read it.
|
|
301
|
-
|
|
302
|
-
The change is unchanged since that attempt (patch-id ${patchId}) and is still measured against ${shortSha(baseRev)}. Other roles merge on main while you review, so compare against ${shortSha(baseRev)}, never against \`main\`.
|
|
303
|
-
|
|
304
|
-
Finish the checks you had not completed, then end your reply with exactly one line in this form:
|
|
305
|
-
${VERDICT_ENDING.replace(/\n/g, "\n ")}`;
|
|
306
|
-
}
|
|
307
|
-
/** The one-turn follow-up sent into the REVIEWER's own session (--continue) when its run
|
|
308
|
-
* completed but the reply carried no parseable VERDICT line — the review-gate sibling of
|
|
309
|
-
* prompt-followup.ts's buildSummaryRequestPrompt. The session already holds the full review, so one
|
|
310
|
-
* short reply recovers the verdict the gate needs without paying a second full review run;
|
|
311
|
-
* the caller bounds the run tightly and counts the strike against the HEAD only when this
|
|
312
|
-
* too yields nothing (BUGS.md 2026-09-29). It shares the review prompt's closing rule
|
|
313
|
-
* (reply-contract.ts's VERDICT_ENDING), so the accepted forms cannot drift between the prompts. */
|
|
314
|
-
export function buildVerdictRequestPrompt() {
|
|
315
|
-
return `Your review reply above did not include the verdict line tumwater parses, so
|
|
316
|
-
your judgment was lost. Reply now with ONLY the closing block — no tool calls, no other text:
|
|
317
|
-
one line in this form:
|
|
318
|
-
${VERDICT_ENDING}`;
|
|
319
|
-
}
|
|
320
|
-
/** The no-re-run nudge (BUGS.md 2026-10-02): the reviewer broke the prompt's no-re-run rule
|
|
321
|
-
* despite a green pre-check, so this turn on its own session names the exact tool call and
|
|
322
|
-
* asks it to finish the review from what it already read. The reviewer's only output channel
|
|
323
|
-
* is the verdict, so the closing rule names both accepted forms like the review prompt does. */
|
|
324
|
-
export function buildNoRerunPrompt(rerun) {
|
|
325
|
-
return `You started a tool call tumwater's pre-check already covered, so it must not run:
|
|
326
|
-
${rerun}
|
|
327
|
-
The full check passed at this exact tree before your review began. Do not re-run it — not in
|
|
328
|
-
this worktree, not in a scratch copy. Finish the review now from what you have already read:
|
|
329
|
-
reply with ONLY the closing block — no tool calls, no other text — one line in this form:
|
|
330
|
-
${VERDICT_ENDING}`;
|
|
331
|
-
}
|
|
332
289
|
/** The one-turn follow-up sent into a run's OWN session (--continue) when the harness's
|
|
333
290
|
* pre-queue self-check found problems likely to get the change rejected. Only some findings are
|
|
334
291
|
* the merge gate's own deterministic checks; the rest (path drift, plan dependencies) are
|
|
@@ -96,63 +96,70 @@ function holdWording(state) {
|
|
|
96
96
|
return "eligible";
|
|
97
97
|
if (state === "refused")
|
|
98
98
|
return "refused (only the operator clears it)"; // vocabulary-ok: the plan-mandated refused hold wording refers to the tumwater operator
|
|
99
|
-
if (state === "needs-review"
|
|
100
|
-
return "needs review";
|
|
99
|
+
if (state === "needs-review")
|
|
100
|
+
return "needs review";
|
|
101
101
|
return `waits on ${state.blockedBy.join(", ")}`;
|
|
102
102
|
}
|
|
103
103
|
/** The one summary line of a named series' planned-part state. */
|
|
104
104
|
function seriesSummary(info) {
|
|
105
105
|
const counts = [];
|
|
106
106
|
if (info.review > 0)
|
|
107
|
-
counts.push(
|
|
107
|
+
counts.push(plural(info.review, "needs review", "need review"));
|
|
108
108
|
if (info.waiting > 0)
|
|
109
|
-
counts.push(`${info.waiting
|
|
109
|
+
counts.push(`${plural(info.waiting, "waits", "wait")} on an earlier part`);
|
|
110
110
|
counts.push(info.refused === 0 ? "none refused" : `${info.refused} refused`);
|
|
111
111
|
return `${plural(info.plannedCount, "part")} planned: ${counts.join(", ")}`;
|
|
112
112
|
}
|
|
113
|
-
/**
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const series = seriesInfos(planned, donePlanTitles);
|
|
117
|
-
const allParts = plannedPartKeys(planned);
|
|
113
|
+
/** Index each live series' first word to its count, so a bare first word names a series only
|
|
114
|
+
* when no other live series shares it. */
|
|
115
|
+
function firstWordCountsOf(series) {
|
|
118
116
|
const firstWordCounts = new Map();
|
|
119
117
|
for (const info of series.values()) {
|
|
120
118
|
const w = firstWord(info.name);
|
|
121
119
|
firstWordCounts.set(w, (firstWordCounts.get(w) ?? 0) + 1);
|
|
122
120
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
return firstWordCounts;
|
|
122
|
+
}
|
|
123
|
+
/** The state a named part renders for one part key: its planned hold, `done`, or `not in the
|
|
124
|
+
* backlog`. */
|
|
125
|
+
function partStateOf(info, key) {
|
|
126
|
+
return info.holds.has(key) ? info.holds.get(key) : info.done.has(key) ? "done" : "not-in-backlog";
|
|
127
|
+
}
|
|
128
|
+
/** Name the live series a line mentions: by full name, or by its sole-shared first word; the
|
|
129
|
+
* `i/n` part refs of that series fill `namedParts` (under a `\u0000`-joined key) and a line with
|
|
130
|
+
* a series token but no part ref names the series. */
|
|
131
|
+
function nameSeriesInLine(line, series, firstWordCounts, namedSeries, namedParts) {
|
|
132
|
+
for (const [sLower, info] of series) {
|
|
133
|
+
const hasFull = lineHas(line, info.name.toLowerCase());
|
|
134
|
+
const fw = firstWord(info.name);
|
|
135
|
+
const hasFirst = firstWordCounts.get(fw) === 1 && lineHas(line, fw);
|
|
136
|
+
if (!hasFull && !hasFirst)
|
|
130
137
|
continue;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const hasFirst = firstWordCounts.get(fw) === 1 && lineHas(line, fw);
|
|
135
|
-
if (!hasFull && !hasFirst)
|
|
136
|
-
continue;
|
|
137
|
-
const refs = partRefsInLine(line).filter((r) => r.of === info.of);
|
|
138
|
-
if (refs.length === 0) {
|
|
139
|
-
namedSeries.add(sLower);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
for (const r of refs) {
|
|
143
|
-
const key = partKey(info.name, r.part);
|
|
144
|
-
const state = info.holds.has(key) ? info.holds.get(key) : info.done.has(key) ? "done" : "not-in-backlog";
|
|
145
|
-
namedParts.set(`${sLower}\u0000${r.part}\u0000${r.of}`, { seriesLower: sLower, part: r.part, of: r.of, state });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
138
|
+
const refs = partRefsInLine(line).filter((r) => r.of === info.of);
|
|
139
|
+
if (refs.length === 0) {
|
|
140
|
+
namedSeries.add(sLower);
|
|
148
141
|
}
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
else {
|
|
143
|
+
for (const r of refs) {
|
|
144
|
+
const state = partStateOf(info, partKey(info.name, r.part));
|
|
145
|
+
namedParts.set(`${sLower}\u0000${r.part}\u0000${r.of}`, { seriesLower: sLower, part: r.part, of: r.of, state });
|
|
153
146
|
}
|
|
154
147
|
}
|
|
155
148
|
}
|
|
149
|
+
}
|
|
150
|
+
/** Name a planned entry or Open bug whose first six words (entryKey-normalized) appear in the
|
|
151
|
+
* line. */
|
|
152
|
+
function nameEntriesInLine(line, allEntries, allParts, plannedCount, namedEntries) {
|
|
153
|
+
for (const [i, entry] of allEntries.entries()) {
|
|
154
|
+
const phrase = firstSixWords(entryKey(entry.title));
|
|
155
|
+
if (phrase !== "" && line.includes(phrase)) {
|
|
156
|
+
namedEntries.set(entryKey(entry.title), { title: entry.title, file: i < plannedCount ? "plans" : "bugs", state: entryHoldAgainst(entry, allParts) });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Render the named series summaries, part lines and entry lines, truncating to `MAX_BLOCK_LINES`
|
|
161
|
+
* with a remainder line naming the dropped items; undefined when nothing live was named. */
|
|
162
|
+
function renderNamedItems(namedSeries, namedParts, namedEntries, series) {
|
|
156
163
|
const lines = [];
|
|
157
164
|
for (const sLower of namedSeries) {
|
|
158
165
|
const info = series.get(sLower);
|
|
@@ -175,3 +182,25 @@ export function notesBacklogBlock(notes, backlog) {
|
|
|
175
182
|
const shown = lines.slice(0, cap - 1);
|
|
176
183
|
return [HEADER, ...shown, `… and ${plural(lines.length - shown.length, "more named item")}`].join("\n");
|
|
177
184
|
}
|
|
185
|
+
/** Build the live backlog block: index each series' first word (`firstWordCountsOf`), read every
|
|
186
|
+
* notes line naming the series, parts and entries it mentions (`nameSeriesInLine`/
|
|
187
|
+
* `nameEntriesInLine`), then render and truncate the names (`renderNamedItems`). Returns undefined
|
|
188
|
+
* when the notes name nothing live. */
|
|
189
|
+
export function notesBacklogBlock(notes, backlog) {
|
|
190
|
+
const { planned, openBugs, donePlanTitles } = backlog;
|
|
191
|
+
const series = seriesInfos(planned, donePlanTitles);
|
|
192
|
+
const allParts = plannedPartKeys(planned);
|
|
193
|
+
const firstWordCounts = firstWordCountsOf(series);
|
|
194
|
+
const allEntries = [...planned, ...openBugs];
|
|
195
|
+
const namedSeries = new Set();
|
|
196
|
+
const namedParts = new Map();
|
|
197
|
+
const namedEntries = new Map();
|
|
198
|
+
for (const rawLine of notes.split("\n")) {
|
|
199
|
+
const line = collapseWhitespace(rawLine).toLowerCase();
|
|
200
|
+
if (line === "")
|
|
201
|
+
continue;
|
|
202
|
+
nameSeriesInLine(line, series, firstWordCounts, namedSeries, namedParts);
|
|
203
|
+
nameEntriesInLine(line, allEntries, allParts, planned.length, namedEntries);
|
|
204
|
+
}
|
|
205
|
+
return renderNamedItems(namedSeries, namedParts, namedEntries, series);
|
|
206
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { readRegularTextOrNull } from "../files/files.js";
|
|
3
3
|
import { truncateWithNote } from "../text/text.js";
|
|
4
4
|
/** Cap on the PRINCIPLES.md text injected into every prompt, so a runaway file cannot blow up
|
|
5
5
|
* each run's prefill. */
|
|
@@ -7,10 +7,12 @@ export const PRINCIPLES_MAX_CHARS = 4000;
|
|
|
7
7
|
/** The project's design principles (PRINCIPLES.md), capped for injection into prompts. Empty
|
|
8
8
|
* string when the file is missing or unreadable — prompt building must never throw on it.
|
|
9
9
|
* (Extracted from prompt.ts, which now builds prompts on top of this reader; the truncation
|
|
10
|
-
* marker format is truncateWithNote's in text.ts.)
|
|
10
|
+
* marker format is truncateWithNote's in text.ts.) A symlinked PRINCIPLES.md is read as empty
|
|
11
|
+
* (readRegularTextOrNull never follows a symlink out of the worktree, INV-G3/G4's lstat
|
|
12
|
+
* policy), so a hostile tree cannot inject an arbitrary host file into every run's prompt. */
|
|
11
13
|
export function readPrinciples(root) {
|
|
12
14
|
const file = path.join(root, "PRINCIPLES.md");
|
|
13
|
-
return truncateWithNote(
|
|
15
|
+
return truncateWithNote(readRegularTextOrNull(file)?.trim() ?? "", PRINCIPLES_MAX_CHARS, "PRINCIPLES.md");
|
|
14
16
|
}
|
|
15
17
|
/** True when PRINCIPLES.md's trimmed text exceeds the prompt cap, so every run's <principles>
|
|
16
18
|
* block carries only its head plus a truncation marker — the always-on amber twin of the doctor
|
|
@@ -20,6 +22,6 @@ export function readPrinciples(root) {
|
|
|
20
22
|
* (the directory-in-place case readPrinciples degrades to "") and at-or-under-cap files are all
|
|
21
23
|
* false. */
|
|
22
24
|
export function principlesBlockOverCap(root) {
|
|
23
|
-
const raw =
|
|
25
|
+
const raw = readRegularTextOrNull(path.join(root, "PRINCIPLES.md"));
|
|
24
26
|
return raw !== null && raw.trim().length > PRINCIPLES_MAX_CHARS;
|
|
25
27
|
}
|
|
@@ -68,16 +68,24 @@ ${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"
|
|
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) {
|
|
72
75
|
const note = cause === "budget-capped"
|
|
73
76
|
? `An earlier API-billed claude run of the "${roleId}" role hit its per-run budget cap before it
|
|
74
77
|
could finish; its uncommitted edits are in your worktree — review \`git diff\`, then finish or
|
|
75
78
|
discard them. This run starts on the model job the daily budget left you.`
|
|
76
|
-
: cause === "
|
|
77
|
-
? `An earlier run of the "${roleId}" role
|
|
79
|
+
: cause === "image-reroute"
|
|
80
|
+
? `An earlier run of the "${roleId}" role on ${imageReroute?.from ?? "another model"} stopped
|
|
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;
|
|
78
86
|
its uncommitted edits were recovered and are in your worktree — review \`git status\` and
|
|
79
87
|
\`git diff\` (new files show as untracked), then finish or discard them.`
|
|
80
|
-
|
|
88
|
+
: `An earlier run of the "${roleId}" role on another model was interrupted; its
|
|
81
89
|
uncommitted edits are in your worktree — review \`git diff\`, then finish or discard them.`;
|
|
82
90
|
return `${runPrompt}\n\n${note}`;
|
|
83
91
|
}
|
|
@@ -82,6 +82,20 @@ export const KILL_BY_PID_RULE = `Never kill processes by name or pattern (\`pkil
|
|
|
82
82
|
export const TEST_RUNNER_RULE = `- Run tests only through the project's declared check or the test framework it already uses —
|
|
83
83
|
never a runner you guessed (say, \`npx vitest\` in a suite written for node:test): a mismatched
|
|
84
84
|
runner finds no tests at best, and can kill them mid-run, leaking processes and temp files.`;
|
|
85
|
+
/** The synthetic-load rule every run that may run commands carries, word for word: the run and
|
|
86
|
+
* director rules (full and compact). The bounding sentence is the point — a loader that ends with
|
|
87
|
+
* the command cannot leak. 2026-10-28: seven leaked \`while :; do :; done\` loops from bugfix runs
|
|
88
|
+
* reproducing a load flake stood at PPID 1 loading the shared host (BUGS.md): macOS \`ps -E\`
|
|
89
|
+
* hides the environment of platform binaries like bash, so the end-of-run marker sweep cannot
|
|
90
|
+
* attribute them to a run to reap them. The only reliable prevention is never starting unbounded
|
|
91
|
+
* load in the first place — a leaked platform-binary loader cannot be reaped on macOS by any
|
|
92
|
+
* sweep that exists. */
|
|
93
|
+
export const SYNTHETIC_LOAD_RULE = `The host is shared by every run, review, and check on the team,
|
|
94
|
+
and already under load; never add synthetic CPU load (a \`while :; do :; done\` spinner, a
|
|
95
|
+
CPU-spinning node process) to reproduce a load flake unless it ends with the command that starts
|
|
96
|
+
it — a \`timeout\`, or backgrounded and killed with \`kill $!\` before that tool call returns. A
|
|
97
|
+
loader that outlives its command is an orphan no sweep can reliably reap, and loads the host
|
|
98
|
+
every sibling run and check runs on until a \`tumwater doctor\` sweep or a human stops it.`;
|
|
85
99
|
/** Render a dialect's `backendRules` as one bullet each, or nothing when the dialect has none
|
|
86
100
|
* (pi and claude) — so a backend's extra shell/reasoning guidance rides the reading-budget
|
|
87
101
|
* section right after the oversized-output rule. */
|
|
@@ -128,9 +142,9 @@ export const DEFAULT_SCOPE = `- Do exactly ONE focused task, then stop. Small, c
|
|
|
128
142
|
* ~2.5k tokens of every run before its first edit (2026-10-17 audit). */
|
|
129
143
|
function commonRules(check, briefFile = "README.md", scope = DEFAULT_SCOPE, dialect = PI_DIALECT, assigned = false, gateCommand) {
|
|
130
144
|
const verify = check && gateCommand
|
|
131
|
-
? `verify with
|
|
132
|
-
after your change and fix what you broke. Tumwater runs the full
|
|
133
|
-
itself; do not run it.`
|
|
145
|
+
? `verify the change with a compile or the one or two test files that cover it — run them
|
|
146
|
+
after your change and fix what you broke. Tumwater runs the full gate (\`${gateCommand}\`)
|
|
147
|
+
itself before the change merges; do not run it.`
|
|
134
148
|
: check
|
|
135
149
|
? `verify with ${describeCheck(check)} (the project's declared check) — run it after your
|
|
136
150
|
change and fix what you broke.`
|
|
@@ -178,6 +192,7 @@ Where you work:
|
|
|
178
192
|
servers, watch modes). To test such a program, background it with a hard time limit (at most
|
|
179
193
|
30 minutes) and never give it a real TTY.
|
|
180
194
|
- ${KILL_BY_PID_RULE} A pattern also kills other agents' runs and checks.
|
|
195
|
+
- ${SYNTHETIC_LOAD_RULE}
|
|
181
196
|
- Leave the project working: ${verify} Write the check's full output to a scratch file once
|
|
182
197
|
(\`${gateCommand ? "<gate>" : "<check>"} > <scratch-dir>/check.log 2>&1\`) and \`tail\` it; grep that file for anything else, and
|
|
183
198
|
re-run the check only after an edit changes the tree.
|
|
@@ -307,48 +322,83 @@ from the repo.`,
|
|
|
307
322
|
}
|
|
308
323
|
/** The prompt for one role run. */
|
|
309
324
|
export function buildRunPrompt(input) {
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
const day = today ?? todayStamp();
|
|
325
|
+
const dialect = dialectFor(input.agent);
|
|
326
|
+
const day = input.today ?? todayStamp();
|
|
313
327
|
const parts = [
|
|
328
|
+
...runIdentityBlocks(input.role, input.rejectedReview, day),
|
|
329
|
+
...runContextBlocks(input),
|
|
330
|
+
...runEvidenceBlocks(input),
|
|
331
|
+
...runNotesBlocks(input, day),
|
|
332
|
+
...runTaskBlocks(input),
|
|
333
|
+
...runRulesBlocks(input, dialect),
|
|
334
|
+
];
|
|
335
|
+
return parts.join("\n\n");
|
|
336
|
+
}
|
|
337
|
+
/** The role identity line and the shared worktree + date preamble. */
|
|
338
|
+
function runIdentityBlocks(role, rejectedReview, day) {
|
|
339
|
+
return [
|
|
314
340
|
...(rejectedReview ? [rejectedReview.trim()] : []),
|
|
315
341
|
`You are the "${role.id}" role (${role.title}) on this project's tumwater team — tumwater runs a team of AI roles that develop the project.`,
|
|
316
342
|
...sharedPreamble(day),
|
|
317
343
|
];
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
344
|
+
}
|
|
345
|
+
/** The principles block and the explicit user-request steering block, in that order. */
|
|
346
|
+
function runContextBlocks(input) {
|
|
347
|
+
const parts = [];
|
|
348
|
+
if (input.principles)
|
|
349
|
+
parts.push(principlesBlock(input.principles));
|
|
350
|
+
if (input.userRequest)
|
|
321
351
|
parts.push(`An explicit request from the user, aimed at this role. Treat it as this run's steering;
|
|
322
352
|
your role's rules and the merge gate still apply unchanged. If the request is out of this
|
|
323
|
-
role's scope, say so in your reply instead of doing it anyway.\n<user-request>\n${userRequest.trim()}\n</user-request>`);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
parts
|
|
351
|
-
|
|
353
|
+
role's scope, say so in your reply instead of doing it anyway.\n<user-request>\n${input.userRequest.trim()}\n</user-request>`);
|
|
354
|
+
return parts;
|
|
355
|
+
}
|
|
356
|
+
/** The six rendered evidence blocks, in the fixed order. */
|
|
357
|
+
function runEvidenceBlocks(input) {
|
|
358
|
+
const parts = [];
|
|
359
|
+
if (input.coverage)
|
|
360
|
+
parts.push(input.coverage);
|
|
361
|
+
if (input.ciParity)
|
|
362
|
+
parts.push(input.ciParity);
|
|
363
|
+
if (input.filings)
|
|
364
|
+
parts.push(input.filings);
|
|
365
|
+
if (input.digest)
|
|
366
|
+
parts.push(digestBlock(input.digest));
|
|
367
|
+
if (input.backlogStructure)
|
|
368
|
+
parts.push(input.backlogStructure);
|
|
369
|
+
if (input.backlogIndex)
|
|
370
|
+
parts.push(input.backlogIndex);
|
|
371
|
+
return parts;
|
|
372
|
+
}
|
|
373
|
+
/** The role-notes block (only when a note exists) and the live backlog state block. */
|
|
374
|
+
function runNotesBlocks(input, day) {
|
|
375
|
+
const parts = [];
|
|
376
|
+
if (input.notes && input.notes.trim() !== "")
|
|
377
|
+
parts.push(roleNotesBlock(input.notes, Boolean(input.backlogIndex), input.notesSavedDay, day));
|
|
378
|
+
if (input.notesBacklog)
|
|
379
|
+
parts.push(input.notesBacklog);
|
|
380
|
+
return parts;
|
|
381
|
+
}
|
|
382
|
+
/** The task text and its optional noCheck/plan-target/flake/extra notes. */
|
|
383
|
+
function runTaskBlocks(input) {
|
|
384
|
+
const parts = [];
|
|
385
|
+
parts.push(`Your task this run:\n${input.role.find.trim()}`);
|
|
386
|
+
if (input.noCheck)
|
|
387
|
+
parts.push(input.noCheck.trim());
|
|
388
|
+
if (input.planTargetNote)
|
|
389
|
+
parts.push(input.planTargetNote.trim());
|
|
390
|
+
if (input.flakyChecks)
|
|
391
|
+
parts.push(input.flakyChecks.trim());
|
|
392
|
+
if (input.extraInstructions)
|
|
393
|
+
parts.push(`Additional standing instructions from the user:\n${input.extraInstructions.trim()}`);
|
|
394
|
+
return parts;
|
|
395
|
+
}
|
|
396
|
+
/** The note-write instruction and the shared rules, which close the prompt. */
|
|
397
|
+
function runRulesBlocks(input, dialect) {
|
|
398
|
+
return [
|
|
399
|
+
roleNotesInstruction(),
|
|
400
|
+
commonRules(input.check, input.briefFile, input.role.scope, dialect, input.assigned, input.gateCommand).trim(),
|
|
401
|
+
];
|
|
352
402
|
}
|
|
353
403
|
/** The prompt for a director run, which routes a user request into the project. */
|
|
354
404
|
export function buildDirectorPrompt(userPrompt, principles,
|