tumwater 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The backend-neutral agent runner (plans/agent-backends.md \"The seam\"): everything a run
|
|
3
2
|
* needs regardless of which agent it drives — the spawn, the `tumwater_run` raw-log marker,
|
|
4
|
-
* the watchdog wiring, abort,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and the exit classification.
|
|
8
|
-
*
|
|
3
|
+
* the watchdog wiring, abort, and stderr capture. The backend (src/agent/agent-backend.ts, with
|
|
4
|
+
* pi in src/pi/pi-backend.ts) provides the argv/stdin/env, the native-to-canonical stdout
|
|
5
|
+
* translator, the stderr warning
|
|
6
|
+
* matcher, and the exit classification. This file keeps the live lifecycle — pre-spawn setup,
|
|
7
|
+
* `prepare`, spawn, decoders, the stream pipeline, the watchdogs and the feed — and delegates
|
|
8
|
+
* the post-run settlement (spawn-error result, exit orphan sweep, close-time reconcile and
|
|
9
|
+
* classify) to src/agent/run-settle.ts. `runPi` (src/pi/pi.ts) is the thin alias the existing
|
|
10
|
+
* call sites use, so a caller and its fakes are untouched by this split. */
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
10
12
|
import fs from "node:fs";
|
|
11
13
|
import os from "node:os";
|
|
12
14
|
import path from "node:path";
|
|
13
15
|
import { StringDecoder } from "node:string_decoder";
|
|
14
|
-
import { ensureDir, ensureParentDir, rotateIfLarge } from "../files/files.js";
|
|
15
|
-
import { agentBinConfigKey, agentBinSourceLabel } from "../pi/pi-bin.js";
|
|
16
|
+
import { ensureDir, ensureParentDir, removeTree, rotateIfLarge } from "../files/files.js";
|
|
16
17
|
import { runOnAbort, terminateChild } from "../process/process.js";
|
|
17
|
-
import { makeRunMarker
|
|
18
|
+
import { makeRunMarker } from "../process/run-marker.js";
|
|
18
19
|
import { PiStreamParser } from "../pi/pi-stream.js";
|
|
19
20
|
import { startPiWatchdogs } from "../pi/pi-watchdogs.js";
|
|
20
|
-
import {
|
|
21
|
+
import { DEFAULT_BACKEND, backendById, registerAgentBackend } from "./agent-backend.js";
|
|
21
22
|
import { recordSession } from "./session-ledger.js";
|
|
23
|
+
import { errorMessage } from "../text/text.js";
|
|
24
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
22
25
|
import { piBackend } from "../pi/pi-backend.js";
|
|
23
26
|
import { claudeBackend } from "./claude/claude-backend.js";
|
|
27
|
+
import { codexBackend } from "./codex/codex-backend.js";
|
|
24
28
|
import { opencodeBackend } from "./opencode/opencode-backend.js";
|
|
29
|
+
import { setupFailureResult, settleClose, settleError, settleExit } from "./run-settle.js";
|
|
25
30
|
import { seamImageInput } from "./image-input.js";
|
|
26
31
|
import { ensureNotesShim } from "./notes-shim.js";
|
|
27
32
|
import { nearestTumwaterRoot } from "../paths.js";
|
|
@@ -34,7 +39,7 @@ const STDERR_MATCH_TAIL = 512;
|
|
|
34
39
|
* object; a line this long is already at the default whole-log cap (`logMaxBytes`, 16 MiB), so
|
|
35
40
|
* continuing to buffer it can only be a malformed stream. Past it `feedCanonical` drops the
|
|
36
41
|
* buffered line and skips input to the next newline, so a backend that streams bytes with no
|
|
37
|
-
* newline — each byte resetting the quiet watchdog — cannot grow the buffer until the
|
|
42
|
+
* newline — each byte resetting the quiet watchdog — cannot grow the buffer until the run
|
|
38
43
|
* timeout (4 h by default) exhausts the harness's memory. */
|
|
39
44
|
export const MAX_NATIVE_LINE_CHARS = 16 * 1024 * 1024;
|
|
40
45
|
// The seam's adapters. Registering here — where the lookup happens — keeps the registration
|
|
@@ -42,20 +47,7 @@ export const MAX_NATIVE_LINE_CHARS = 16 * 1024 * 1024;
|
|
|
42
47
|
registerAgentBackend(piBackend);
|
|
43
48
|
registerAgentBackend(claudeBackend);
|
|
44
49
|
registerAgentBackend(opencodeBackend);
|
|
45
|
-
|
|
46
|
-
* is created for such a run). The resolved binary follows it, so a configured agentBin
|
|
47
|
-
* reads "failed to spawn <bin>" rather than blaming an ambient "pi". */
|
|
48
|
-
const SPAWN_ERROR_PREFIX = "failed to spawn";
|
|
49
|
-
/** The spawn-error message for a resolved binary: the default source keeps today's text
|
|
50
|
-
* ("failed to spawn pi: …"); a configured source names the binary and where it came from —
|
|
51
|
-
* the backend's own env var and config key (agentBinConfigKey) — so a wrong
|
|
52
|
-
* `backends.claude.bin` does not read as pi's legacy `agentBin`. */
|
|
53
|
-
function spawnErrorMessage(resolved, errMessage, backend) {
|
|
54
|
-
const source = resolved.source === "default"
|
|
55
|
-
? ""
|
|
56
|
-
: ` (resolved from ${agentBinSourceLabel(resolved.source, backend.binEnv, agentBinConfigKey(backend.id))})`;
|
|
57
|
-
return `${SPAWN_ERROR_PREFIX} ${resolved.bin}${source}: ${errMessage}`;
|
|
58
|
-
}
|
|
50
|
+
registerAgentBackend(codexBackend);
|
|
59
51
|
function openAppendLog(file, maxBytes) {
|
|
60
52
|
ensureParentDir(file);
|
|
61
53
|
rotateIfLarge(file, maxBytes);
|
|
@@ -66,449 +58,474 @@ function openAppendLog(file, maxBytes) {
|
|
|
66
58
|
return log;
|
|
67
59
|
}
|
|
68
60
|
/** Run the configured agent backend non-interactively in a worktree and distill the result.
|
|
69
|
-
* Never throws
|
|
61
|
+
* Never throws: a setup failure (session dir, log file, scratch dir) resolves as a failed run
|
|
62
|
+
* whose errorMessage names it. */
|
|
70
63
|
export function runAgent(opts) {
|
|
71
|
-
|
|
64
|
+
// An async executor so a backend's `prepare` step (the codex write guard resolving its trust
|
|
65
|
+
// by spawning app-server) can be awaited before the synchronous invocation is built. A
|
|
66
|
+
// `prepare` throw is caught and warned, never rejecting the run; a synchronous throw in the
|
|
67
|
+
// build-and-spawn stage below (e.g. claude's invocation rejecting a run with no model)
|
|
68
|
+
// rejects the run, matching what the Promise constructor did for a throw from a synchronous
|
|
69
|
+
// executor.
|
|
70
|
+
return new Promise(async (resolve, reject) => {
|
|
72
71
|
// The `agent` selector is optional: default to pi, the historical backend. An unregistered
|
|
73
|
-
// id also falls back to pi rather than rejecting the
|
|
74
|
-
const backend =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
72
|
+
// id also falls back to pi rather than rejecting the run.
|
|
73
|
+
const backend = backendById(opts.config.agent ?? DEFAULT_BACKEND) ?? piBackend;
|
|
74
|
+
let rawLog;
|
|
75
|
+
let nativeLog;
|
|
76
|
+
let runTmp;
|
|
77
|
+
try {
|
|
78
|
+
ensureDir(opts.sessionDir);
|
|
79
|
+
// A broken raw log (EACCES, ENOSPC) must not crash the process on an unhandled 'error'
|
|
80
|
+
// nor hang the run: openAppendLog marks it so finish() resolves without waiting for a
|
|
81
|
+
// 'finish' that will never come — a lost transcript is acceptable, a stuck runner is not.
|
|
82
|
+
rawLog = openAppendLog(opts.rawLogFile, opts.config.logMaxBytes);
|
|
83
|
+
// The backend-native stdout, kept beside the canonical raw log for debugging: every
|
|
84
|
+
// complete native line is written before it is translated, so the two logs line up. A
|
|
85
|
+
// broken native log degrades to a lost debug log, never a stuck or failed run.
|
|
86
|
+
nativeLog = openAppendLog(opts.nativeLogFile, opts.config.logMaxBytes);
|
|
87
|
+
// A private TMPDIR per run: concurrent agents default to literal /tmp names, and a shared
|
|
88
|
+
// per-user TMPDIR would still collide. Removed when the run settles.
|
|
89
|
+
runTmp = fs.mkdtempSync(path.join(os.tmpdir(), "tumwater-run-"));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
// Nothing is spawned yet: close what opened and fail the run instead of rejecting it.
|
|
93
|
+
rawLog?.stream.end();
|
|
94
|
+
nativeLog?.stream.end();
|
|
95
|
+
resolve(setupFailureResult(backend.id, `could not prepare the agent run: ${errorMessage(err)}`));
|
|
96
|
+
return;
|
|
97
97
|
}
|
|
98
|
-
// The
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
98
|
+
// The pre-spawn setup below may throw synchronously (a broken marker write, parser
|
|
99
|
+
// construction, backend.resolveBin). An async executor's own rejection is swallowed by the
|
|
100
|
+
// Promise constructor, so a throw here would otherwise leave the run's promise pending
|
|
101
|
+
// forever and emit an unhandled rejection — a synchronous executor used to reject directly.
|
|
102
|
+
// The try/catch below restores that rejection. A known-recoverable failure (the notes
|
|
103
|
+
// shim) warns and goes on inside its own guard, so it is deliberately not covered here.
|
|
103
104
|
let blindRead;
|
|
104
105
|
let terminateRun;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// false — which the parser checks against its own note rule.
|
|
109
|
-
imageInput: seamImageInput(opts.config, readPiProviders(piModelsPath())),
|
|
110
|
-
onBlindRead: (path) => {
|
|
111
|
-
// Kill an authoring run at once, before the agent goes on working from a picture it
|
|
112
|
-
// never saw (terminateChild is what the watchdogs use). Gate runs (reviewer,
|
|
113
|
-
// resolver) only record the read and keep going.
|
|
114
|
-
if (opts.kind === "author") {
|
|
115
|
-
blindRead = path;
|
|
116
|
-
terminateRun?.();
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
// The run's cross-group attribution mark, minted before the spawn so the environment can
|
|
121
|
-
// carry it and the exit sweep can name it.
|
|
122
|
-
const runMarker = makeRunMarker();
|
|
123
|
-
const resolved = backend.resolveBin(opts.config);
|
|
124
|
-
// Install the notes shim under the team root's `.tumwater/bin` so every backend's run finds
|
|
125
|
-
// `tumwater-notes` first on PATH (notes-shim.ts). A failure to write it logs one warning and
|
|
126
|
-
// the run goes on without it — notes would then be unwritable by the command, but the run
|
|
127
|
-
// itself is not blocked.
|
|
106
|
+
let parser;
|
|
107
|
+
let runMarker;
|
|
108
|
+
let resolved;
|
|
128
109
|
let binDir;
|
|
110
|
+
let runSpec;
|
|
129
111
|
try {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
112
|
+
// The marker precedes this run's first event in file order — written to the same stream
|
|
113
|
+
// stdout lines flow through, before spawn, so ordering is exact by construction; on a
|
|
114
|
+
// failed spawn finish() still ends the stream and flushes it. Exactly one per run: the
|
|
115
|
+
// kind, agent and start time are always there, the label only when the caller set one
|
|
116
|
+
// (`history <role> <n> --transcript` selects a run's segments by this ts).
|
|
117
|
+
{
|
|
118
|
+
const marker = {
|
|
119
|
+
type: "tumwater_run",
|
|
120
|
+
kind: opts.kind,
|
|
121
|
+
agent: backend.id,
|
|
122
|
+
ts: Date.now(),
|
|
123
|
+
};
|
|
124
|
+
if (opts.label)
|
|
125
|
+
marker.label = opts.label;
|
|
126
|
+
rawLog.stream.write(JSON.stringify(marker) + "\n");
|
|
127
|
+
}
|
|
128
|
+
// The first blind image read an authoring run made (plans/image-input.md "Detecting a
|
|
129
|
+
// blind read"): the path, plus an indirection to the child so the parser's parse-time
|
|
130
|
+
// callback (which fires during feed, after spawn) can kill it. Unset for a gate run,
|
|
131
|
+
// which only records the read. Declared before the parser so its onBlindRead closure
|
|
132
|
+
// (which assigns them at parse time, after spawn) can reference them.
|
|
133
|
+
parser = new PiStreamParser(opts.onToolCallStart, {
|
|
134
|
+
// The seam's effective image input (src/agent/image-input.ts): a text-only seam makes
|
|
135
|
+
// every image read blind even when pi adds no note — a claim its doctor probe proved
|
|
136
|
+
// false — which the parser checks against its own note rule.
|
|
137
|
+
imageInput: seamImageInput(opts.config, readPiProviders(piModelsPath())),
|
|
138
|
+
onBlindRead: (path) => {
|
|
139
|
+
// Kill an authoring run at once, before the agent goes on working from a picture it
|
|
140
|
+
// never saw (terminateChild is what the watchdogs use). Gate runs (reviewer,
|
|
141
|
+
// resolver) only record the read and keep going.
|
|
142
|
+
if (opts.kind === "author") {
|
|
143
|
+
blindRead = path;
|
|
144
|
+
terminateRun?.();
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// The run's cross-group attribution mark, minted before the spawn so the environment can
|
|
149
|
+
// carry it and the exit sweep can name it.
|
|
150
|
+
runMarker = makeRunMarker();
|
|
151
|
+
resolved = backend.resolveBin(opts.config);
|
|
152
|
+
// Install the notes shim under the team root's `.tumwater/bin` so every backend's run
|
|
153
|
+
// finds `tumwater-notes` first on PATH (notes-shim.ts). A failure to write it logs one
|
|
154
|
+
// warning and the run goes on without it — notes would then be unwritable by the command,
|
|
155
|
+
// but the run itself is not blocked.
|
|
156
|
+
try {
|
|
157
|
+
// Install the shim only under a real team root — the nearest ancestor holding a
|
|
158
|
+
// `.tumwater`. A session dir with no such ancestor (a lab script, a smoke run, a
|
|
159
|
+
// prompt-lab replay) must never mint one: nearestTumwaterRoot is undefined then, and
|
|
160
|
+
// the run goes on without the shim, exactly as if the install had failed — writing
|
|
161
|
+
// `<fallback>/.tumwater` from a run created a phantom harness root in a shared
|
|
162
|
+
// directory and broke every other suite on the host (BUGS.md 2026-10-18).
|
|
163
|
+
const root = nearestTumwaterRoot(opts.sessionDir);
|
|
164
|
+
if (root !== undefined)
|
|
165
|
+
binDir = ensureNotesShim(root);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
opts.onAgentWarning?.("could not install the tumwater-notes shim; this run will not find tumwater-notes on PATH");
|
|
169
|
+
}
|
|
170
|
+
runSpec = {
|
|
171
|
+
cwd: opts.cwd,
|
|
172
|
+
prompt: opts.prompt,
|
|
173
|
+
config: opts.config,
|
|
174
|
+
sessionDir: opts.sessionDir,
|
|
175
|
+
sessionName: opts.sessionName,
|
|
176
|
+
continueSession: opts.continueSession,
|
|
177
|
+
kind: opts.kind,
|
|
178
|
+
agentBin: resolved.bin,
|
|
179
|
+
runMarker,
|
|
180
|
+
notesPath: opts.notesPath,
|
|
181
|
+
...(binDir !== undefined ? { binDir } : {}),
|
|
182
|
+
...(opts.maxBudgetUsd !== undefined ? { maxBudgetUsd: opts.maxBudgetUsd } : {}),
|
|
183
|
+
};
|
|
139
184
|
}
|
|
140
|
-
catch {
|
|
141
|
-
|
|
185
|
+
catch (err) {
|
|
186
|
+
// A synchronous throw in the run's pre-spawn setup (marker, parser construction,
|
|
187
|
+
// resolveBin) rejects the run, restoring the rejection a synchronous executor's throw
|
|
188
|
+
// used to get from the Promise constructor.
|
|
189
|
+
reject(err instanceof Error ? err : new Error(`runAgent: ${String(err)}`));
|
|
190
|
+
return;
|
|
142
191
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const invocation = backend.invocation(runSpec);
|
|
158
|
-
// Record the run in the session ledger before its spawn, so a later resume can pick the
|
|
159
|
-
// newest session of its own kind instead of whatever file is newest in the dir. The id is
|
|
160
|
-
// the backend's own when it mints one ahead of the run (claude's UUID, or the resumed
|
|
161
|
-
// record's id); pi mints none and keeps the session name.
|
|
162
|
-
recordSession(opts.sessionDir, {
|
|
163
|
-
agent: backend.id,
|
|
164
|
-
id: invocation.sessionId ?? opts.sessionName,
|
|
165
|
-
kind: opts.kind,
|
|
166
|
-
name: opts.sessionName,
|
|
167
|
-
cwd: opts.cwd,
|
|
168
|
-
...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
|
|
169
|
-
at: Date.now(),
|
|
170
|
-
});
|
|
171
|
-
const translator = backend.translator();
|
|
172
|
-
// Decode stdout incrementally instead of per chunk: a raw Buffer.toString("utf8")
|
|
173
|
-
// replaces any multi-byte character whose bytes straddle two 'data' events with U+FFFD,
|
|
174
|
-
// corrupting that line's text (commit subjects, summaries, transcripts). StringDecoder
|
|
175
|
-
// holds back the incomplete trailing bytes until the next chunk completes them.
|
|
176
|
-
const decoder = new StringDecoder("utf8");
|
|
177
|
-
// stderr gets its own incremental decoder for the same reason: the failure message and
|
|
178
|
-
// the stderr-keyed matchers (crash, stream-severed, config) read this buffer, so a
|
|
179
|
-
// character whose bytes straddle two 'data' events must not become U+FFFD.
|
|
180
|
-
const stderrDecoder = new StringDecoder("utf8");
|
|
181
|
-
let stderr = "";
|
|
182
|
-
// The fallback-clone warning arrives at the START of a run, while `stderr` keeps only the
|
|
183
|
-
// last 64 KiB; match each chunk (with a short carried tail for a straddle) so a chatty run
|
|
184
|
-
// that pushes the warning out of that window still surfaces the mispriced id.
|
|
185
|
-
let fallbackClone;
|
|
186
|
-
let stderrTail = "";
|
|
187
|
-
let settled = false;
|
|
188
|
-
// The translator's native line buffer: feed the decoder's text through it and only complete
|
|
189
|
-
// lines reach the translator, whose canonical output feeds the parser (and the raw log).
|
|
190
|
-
// `nativeOverflow` marks a buffered line that passed MAX_NATIVE_LINE_CHARS without a newline:
|
|
191
|
-
// its remainder is skipped to the next newline so it can never be mistaken for an event.
|
|
192
|
-
let nativeTail = "";
|
|
193
|
-
let nativeOverflow = false;
|
|
194
|
-
// A private TMPDIR per run: concurrent agents default to literal /tmp names, and a shared
|
|
195
|
-
// per-user TMPDIR would still collide. Removed when the run settles.
|
|
196
|
-
const runTmp = fs.mkdtempSync(path.join(os.tmpdir(), "tumwater-run-"));
|
|
197
|
-
const child = spawn(resolved.bin, invocation.args, {
|
|
198
|
-
cwd: opts.cwd,
|
|
199
|
-
// A backend that feeds the prompt on stdin (every registered one) gets a pipe the runner
|
|
200
|
-
// writes and ends below; one that returns no stdin keeps stdio[0] "ignore".
|
|
201
|
-
stdio: [invocation.stdin !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
202
|
-
// pi sets its process title at startup, and so does every npm its tool calls run: on
|
|
203
|
-
// macOS each would register with LaunchServices and leak a launchservicesd port.
|
|
204
|
-
// Detached so pi leads its own process group: terminateChild signals the group, so a
|
|
205
|
-
// run's tool-call grandchildren — killed or finished — die with it instead of leaking
|
|
206
|
-
// to launchd.
|
|
207
|
-
detached: true,
|
|
208
|
-
// The run's mark, inherited by every tool call and its descendants: pi 0.87.1's bash
|
|
209
|
-
// tool spawns each command detached in its OWN group, so the group sweep below cannot
|
|
210
|
-
// reach what a tool call backgrounds (BUGS.md 2026-09-30) — the mark is the only
|
|
211
|
-
// attribution that follows a reparented orphan. Appended to any inherited mark so a
|
|
212
|
-
// nested harness under test keeps the outer run's.
|
|
213
|
-
env: { ...invocation.env, TMPDIR: runTmp },
|
|
214
|
-
});
|
|
215
|
-
if (invocation.stdin !== undefined) {
|
|
216
|
-
// A backend that dies before draining its stdin — an immediately-failing startup such as
|
|
217
|
-
// an unknown flag, an unresumable session, or a not-logged-in exit — leaves this write
|
|
218
|
-
// nothing to feed. Once the prompt exceeds the OS pipe buffer the write then fails EPIPE,
|
|
219
|
-
// and an unhandled 'error' event on the stdin stream is thrown from the event loop and
|
|
220
|
-
// takes the whole process (the whole team) down. The child's own exit status and stderr
|
|
221
|
-
// already report why the run died, so swallowing the failed write adds no silence the
|
|
222
|
-
// caller needs to break; git.ts's patchId carries the same guard for a patch-id that
|
|
223
|
-
// dies before reading its input.
|
|
224
|
-
child.stdin?.on("error", () => { });
|
|
225
|
-
child.stdin?.end(invocation.stdin);
|
|
192
|
+
// A backend's asynchronous pre-run work (the codex write guard resolving its trust by
|
|
193
|
+
// spawning `app-server`, plans/codex-write-guard.md "Trust (parts 4–5)"): awaited just
|
|
194
|
+
// before the synchronous invocation, so trust is settled before invocation reads it. It
|
|
195
|
+
// never throws a run away — a throw is a warm-up failure, warned about on the run's own
|
|
196
|
+
// root (skipped with no root) and the run goes on; a codex trust lookup failure already
|
|
197
|
+
// records `off` and warns from prepare itself, so this catch is the guard for a backend
|
|
198
|
+
// whose prepare throws unexpectedly. The call itself is also guarded: a synchronous
|
|
199
|
+
// `prepare` throw would otherwise escape the async executor (leaving the run's promise
|
|
200
|
+
// pending and emitting an unhandled rejection), so it is warned-and-continued like an
|
|
201
|
+
// async rejection.
|
|
202
|
+
const warnRoot = nearestTumwaterRoot(opts.sessionDir);
|
|
203
|
+
let prepared;
|
|
204
|
+
try {
|
|
205
|
+
prepared = backend.prepare?.(runSpec);
|
|
226
206
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// The run's two watchdog clocks (src/pi/pi-watchdogs.ts): the tick deadline and the quiet
|
|
231
|
-
// watchdog (quiet-kill plus the stalled-tool-call warning). The flags their timers set
|
|
232
|
-
// (timedOut/timedOutProgressing/quietKilled/sawOutput) read live off the returned handle.
|
|
233
|
-
const wd = startPiWatchdogs({
|
|
234
|
-
progressCount: () => parser.progressCount,
|
|
235
|
-
openToolCalls: () => parser.openToolCalls,
|
|
236
|
-
tickTimeoutMs: opts.config.runTimeoutSeconds * 1000,
|
|
237
|
-
quietMs: opts.config.quietTimeoutSeconds * 1000,
|
|
238
|
-
stallMs: Math.max(0, opts.config.toolCallStallSeconds) * 1000,
|
|
239
|
-
// The run's backend id: claude never sends content updates, so the watchdog judges its
|
|
240
|
-
// every open call by the buffered wall-clock threshold (plans/agent-backends.md).
|
|
241
|
-
agent: backend.id,
|
|
242
|
-
// The gate's fresh-session runs (reviewer and conflict resolver) get one deadline
|
|
243
|
-
// extension while they are still making progress: a large diff's review otherwise
|
|
244
|
-
// times out mid-verification and the next attempt re-reads it from scratch
|
|
245
|
-
// (BUGS.md 2026-10-07). Authoring runs resume their session next tick, so one
|
|
246
|
-
// deadline stays right for them.
|
|
247
|
-
extendOnProgress: opts.kind === "gate",
|
|
248
|
-
kill: () => terminateChild(child),
|
|
249
|
-
onToolCallStalled: opts.onToolCallStalled,
|
|
250
|
-
});
|
|
251
|
-
let aborted = false;
|
|
252
|
-
const onAbort = () => {
|
|
253
|
-
aborted = true;
|
|
254
|
-
terminateChild(child);
|
|
255
|
-
};
|
|
256
|
-
runOnAbort(opts.signal, onAbort);
|
|
257
|
-
// Quiet watchdog and its stall warning: src/pi/pi-watchdogs.ts.
|
|
258
|
-
const onLine = (line) => rawLog.stream.write(line + "\n");
|
|
259
|
-
// Write one complete native line and feed its translation to the parser (which also hands
|
|
260
|
-
// the canonical lines to onLine for the raw log).
|
|
261
|
-
const emitNativeLine = (line) => {
|
|
262
|
-
if (!nativeLog.broken)
|
|
263
|
-
nativeLog.stream.write(line + "\n");
|
|
264
|
-
for (const canonical of translator.translate(line))
|
|
265
|
-
parser.feed(canonical + "\n", onLine);
|
|
266
|
-
};
|
|
267
|
-
// Split native stdout into complete lines, translate each, and feed the canonical lines to
|
|
268
|
-
// the parser (which also hands them to onLine for the raw log). A trailing partial native
|
|
269
|
-
// line stays buffered exactly as the parser's own buffer once held it. On the final flush
|
|
270
|
-
// (`final`), a remaining tail is emitted as one line: a backend may close after flushing a
|
|
271
|
-
// complete JSON event but not its newline terminator, and dropping that tail loses the
|
|
272
|
-
// run's authoritative result (cost, final text). A genuinely truncated fragment parses as
|
|
273
|
-
// no data (parseJsonObject → []), so this can only recover the last event.
|
|
274
|
-
const feedCanonical = (text, final = false) => {
|
|
275
|
-
// A buffered overflow: discard this line's remainder up to (and including) its newline, so
|
|
276
|
-
// the stream resynchronizes on the next real event line instead of emitting a truncated one.
|
|
277
|
-
if (nativeOverflow) {
|
|
278
|
-
const nl = text.indexOf("\n");
|
|
279
|
-
if (nl < 0)
|
|
280
|
-
return;
|
|
281
|
-
nativeOverflow = false;
|
|
282
|
-
text = text.slice(nl + 1);
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (warnRoot !== undefined) {
|
|
209
|
+
warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
|
|
283
210
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
start = nl + 1;
|
|
211
|
+
}
|
|
212
|
+
// Only a backend with a `prepare` step (codex) awaits it; for every other backend the run
|
|
213
|
+
// spawns synchronously, preserving the pre-`prepare` timing a logical-clock watchdog test
|
|
214
|
+
// relies on (pi-watchdogs advances a fake clock immediately after runPi returns).
|
|
215
|
+
if (prepared !== undefined) {
|
|
216
|
+
try {
|
|
217
|
+
await prepared;
|
|
292
218
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// No newline within the cap: drop the line and warn. On the final flush there is no
|
|
297
|
-
// remainder to skip, so only the buffer is cleared.
|
|
298
|
-
nativeTail = "";
|
|
299
|
-
if (!final) {
|
|
300
|
-
nativeOverflow = true;
|
|
301
|
-
opts.onAgentWarning?.(`native output line exceeded ${MAX_NATIVE_LINE_CHARS / (1024 * 1024)} MiB without a newline and was discarded`);
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (warnRoot !== undefined) {
|
|
221
|
+
warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
|
|
302
222
|
}
|
|
303
223
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const invocation = backend.invocation(runSpec);
|
|
227
|
+
// The id the pre-spawn ledger record carries: a backend that mints one ahead of the run
|
|
228
|
+
// (claude, a codex resume), else the session name (pi, a fresh codex run — its id is born
|
|
229
|
+
// in the stream). The stream-mint pass below compares against it so a codex run appends
|
|
230
|
+
// its streamed thread id exactly once, and only when it differs from this.
|
|
231
|
+
const preSpawnSessionId = invocation.sessionId ?? opts.sessionName;
|
|
232
|
+
// Record the run in the session ledger before its spawn, so a later resume can pick the
|
|
233
|
+
// newest session of its own kind instead of whatever file is newest in the dir. The id is
|
|
234
|
+
// the backend's own when it mints one ahead of the run (claude's UUID, or the resumed
|
|
235
|
+
// record's id); pi mints none and keeps the session name.
|
|
236
|
+
recordSession(opts.sessionDir, {
|
|
237
|
+
agent: backend.id,
|
|
238
|
+
id: preSpawnSessionId,
|
|
239
|
+
kind: opts.kind,
|
|
240
|
+
name: opts.sessionName,
|
|
241
|
+
cwd: opts.cwd,
|
|
242
|
+
...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
|
|
243
|
+
at: Date.now(),
|
|
244
|
+
});
|
|
245
|
+
const translator = backend.translator();
|
|
246
|
+
// Decode stdout incrementally instead of per chunk: a raw Buffer.toString("utf8")
|
|
247
|
+
// replaces any multi-byte character whose bytes straddle two 'data' events with U+FFFD,
|
|
248
|
+
// corrupting that line's text (commit subjects, summaries, transcripts). StringDecoder
|
|
249
|
+
// holds back the incomplete trailing bytes until the next chunk completes them.
|
|
250
|
+
const decoder = new StringDecoder("utf8");
|
|
251
|
+
// stderr gets its own incremental decoder for the same reason: the failure message and
|
|
252
|
+
// the stderr-keyed matchers (crash, stream-severed, config) read this buffer, so a
|
|
253
|
+
// character whose bytes straddle two 'data' events must not become U+FFFD.
|
|
254
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
255
|
+
let stderr = "";
|
|
256
|
+
// The fallback-clone warning arrives at the START of a run, while `stderr` keeps only the
|
|
257
|
+
// last 64 KiB; match each chunk (with a short carried tail for a straddle) so a chatty run
|
|
258
|
+
// that pushes the warning out of that window still surfaces the mispriced id.
|
|
259
|
+
let fallbackClone;
|
|
260
|
+
let stderrTail = "";
|
|
261
|
+
let settled = false;
|
|
262
|
+
// The translator's native line buffer: feed the decoder's text through it and only complete
|
|
263
|
+
// lines reach the translator, whose canonical output feeds the parser (and the raw log).
|
|
264
|
+
// `nativeOverflow` marks a buffered line that passed MAX_NATIVE_LINE_CHARS without a newline:
|
|
265
|
+
// its remainder is skipped to the next newline so it can never be mistaken for an event.
|
|
266
|
+
let nativeTail = "";
|
|
267
|
+
let nativeOverflow = false;
|
|
268
|
+
const child = spawn(resolved.bin, invocation.args, {
|
|
269
|
+
cwd: opts.cwd,
|
|
270
|
+
// A backend that feeds the prompt on stdin (every registered one) gets a pipe the runner
|
|
271
|
+
// writes and ends below; one that returns no stdin keeps stdio[0] "ignore".
|
|
272
|
+
stdio: [invocation.stdin !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
273
|
+
// pi sets its process title at startup, and so does every npm its tool calls run: on
|
|
274
|
+
// macOS each would register with LaunchServices and leak a launchservicesd port.
|
|
275
|
+
// Detached so pi leads its own process group: terminateChild signals the group, so a
|
|
276
|
+
// run's tool-call grandchildren — killed or finished — die with it instead of leaking
|
|
277
|
+
// to launchd.
|
|
278
|
+
detached: true,
|
|
279
|
+
// The run's mark, inherited by every tool call and its descendants: pi 0.87.1's bash
|
|
280
|
+
// tool spawns each command detached in its OWN group, so the group sweep below cannot
|
|
281
|
+
// reach what a tool call backgrounds (BUGS.md 2026-09-30) — the mark is the only
|
|
282
|
+
// attribution that follows a reparented orphan. Appended to any inherited mark so a
|
|
283
|
+
// nested harness under test keeps the outer run's.
|
|
284
|
+
env: { ...invocation.env, TMPDIR: runTmp },
|
|
285
|
+
});
|
|
286
|
+
if (invocation.stdin !== undefined) {
|
|
287
|
+
// A backend that dies before draining its stdin — an immediately-failing startup such as
|
|
288
|
+
// an unknown flag, an unresumable session, or a not-logged-in exit — leaves this write
|
|
289
|
+
// nothing to feed. Once the prompt exceeds the OS pipe buffer the write then fails EPIPE,
|
|
290
|
+
// and an unhandled 'error' event on the stdin stream is thrown from the event loop and
|
|
291
|
+
// takes the whole process (the whole team) down. The child's own exit status and stderr
|
|
292
|
+
// already report why the run died, so swallowing the failed write adds no silence the
|
|
293
|
+
// caller needs to break; git.ts's patchId carries the same guard for a patch-id that
|
|
294
|
+
// dies before reading its input.
|
|
295
|
+
child.stdin?.on("error", () => { });
|
|
296
|
+
child.stdin?.end(invocation.stdin);
|
|
341
297
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
298
|
+
// Armed now that the child exists, so the parser's parse-time onBlindRead (fires during
|
|
299
|
+
// feed, always after spawn) can reach it.
|
|
300
|
+
terminateRun = () => terminateChild(child);
|
|
301
|
+
// The run's two watchdog clocks (src/pi/pi-watchdogs.ts): the run deadline and the quiet
|
|
302
|
+
// watchdog (quiet-kill plus the stalled-tool-call warning). The flags their timers set
|
|
303
|
+
// (timedOut/timedOutProgressing/quietKilled/sawOutput) read live off the returned handle.
|
|
304
|
+
const wd = startPiWatchdogs({
|
|
305
|
+
progressCount: () => parser.progressCount,
|
|
306
|
+
openToolCalls: () => parser.openToolCalls,
|
|
307
|
+
runTimeoutMs: opts.config.runTimeoutSeconds * 1000,
|
|
308
|
+
quietMs: opts.config.quietTimeoutSeconds * 1000,
|
|
309
|
+
stallMs: Math.max(0, opts.config.toolCallStallSeconds) * 1000,
|
|
310
|
+
// The run's backend id: claude never sends content updates, so the watchdog judges its
|
|
311
|
+
// every open call by the buffered wall-clock threshold (plans/agent-backends.md).
|
|
312
|
+
agent: backend.id,
|
|
313
|
+
// The gate's fresh-session runs (reviewer and conflict resolver) get one deadline
|
|
314
|
+
// extension while they are still making progress: a large diff's review otherwise
|
|
315
|
+
// times out mid-verification and the next attempt re-reads it from scratch
|
|
316
|
+
// (BUGS.md 2026-10-07). Authoring agent runs resume their session on the next one, so one
|
|
317
|
+
// deadline stays right for them.
|
|
318
|
+
extendOnProgress: opts.kind === "gate",
|
|
319
|
+
kill: () => terminateChild(child),
|
|
320
|
+
onToolCallStalled: opts.onToolCallStalled,
|
|
321
|
+
});
|
|
322
|
+
let aborted = false;
|
|
323
|
+
// A blind image read on an authoring run deliberately kills it, like the watchdogs do.
|
|
324
|
+
const wasKilled = () => aborted || wd.timedOut || wd.quietKilled || blindRead !== undefined;
|
|
325
|
+
const onAbort = () => {
|
|
326
|
+
aborted = true;
|
|
327
|
+
terminateChild(child);
|
|
328
|
+
};
|
|
329
|
+
runOnAbort(opts.signal, onAbort);
|
|
330
|
+
// Quiet watchdog and its stall warning: src/pi/pi-watchdogs.ts.
|
|
331
|
+
const onLine = (line) => rawLog.stream.write(line + "\n");
|
|
332
|
+
// The translator's session id, once the stream has named it (codex's `thread.started`).
|
|
333
|
+
// The pre-spawn record for a fresh codex run carries the session name; the id it actually
|
|
334
|
+
// ran with is born in the stream, so the first time this returns one the pre-spawn record
|
|
335
|
+
// lacked, append a second ledger record carrying it (same kind, name, cwd and model) — the
|
|
336
|
+
// record a later `exec resume` resumes (design §4 "Sessions").
|
|
337
|
+
let streamMintedSession = false;
|
|
338
|
+
const emitNativeLine = (line) => {
|
|
339
|
+
if (!nativeLog.broken)
|
|
340
|
+
nativeLog.stream.write(line + "\n");
|
|
341
|
+
for (const canonical of translator.translate(line))
|
|
342
|
+
parser.feed(canonical + "\n", onLine);
|
|
343
|
+
const sid = translator.sessionId?.();
|
|
344
|
+
if (sid !== undefined && sid !== preSpawnSessionId && !streamMintedSession) {
|
|
345
|
+
streamMintedSession = true;
|
|
346
|
+
recordSession(opts.sessionDir, {
|
|
347
|
+
agent: backend.id,
|
|
348
|
+
id: sid,
|
|
349
|
+
kind: opts.kind,
|
|
350
|
+
name: opts.sessionName,
|
|
351
|
+
cwd: opts.cwd,
|
|
352
|
+
...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
|
|
353
|
+
at: Date.now(),
|
|
354
|
+
});
|
|
358
355
|
}
|
|
359
356
|
};
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
357
|
+
// Split native stdout into complete lines, translate each, and feed the canonical lines to
|
|
358
|
+
// the parser (which also hands them to onLine for the raw log). A trailing partial native
|
|
359
|
+
// line stays buffered exactly as the parser's own buffer once held it. On the final flush
|
|
360
|
+
// (`final`), a remaining tail is emitted as one line: a backend may close after flushing a
|
|
361
|
+
// complete JSON event but not its newline terminator, and dropping that tail loses the
|
|
362
|
+
// run's authoritative result (cost, final text). A genuinely truncated fragment parses as
|
|
363
|
+
// no data (parseJsonObject → []), so this can only recover the last event.
|
|
364
|
+
const feedCanonical = (text, final = false) => {
|
|
365
|
+
// A buffered overflow: discard this line's remainder up to (and including) its newline, so
|
|
366
|
+
// the stream resynchronizes on the next real event line instead of emitting a truncated one.
|
|
367
|
+
if (nativeOverflow) {
|
|
368
|
+
const nl = text.indexOf("\n");
|
|
369
|
+
if (nl < 0)
|
|
370
|
+
return;
|
|
371
|
+
nativeOverflow = false;
|
|
372
|
+
text = text.slice(nl + 1);
|
|
373
|
+
}
|
|
374
|
+
nativeTail += text;
|
|
375
|
+
let start = 0;
|
|
376
|
+
for (;;) {
|
|
377
|
+
const nl = nativeTail.indexOf("\n", start);
|
|
378
|
+
if (nl < 0)
|
|
379
|
+
break;
|
|
380
|
+
emitNativeLine(nativeTail.slice(start, nl));
|
|
381
|
+
start = nl + 1;
|
|
382
|
+
}
|
|
383
|
+
if (start > 0)
|
|
384
|
+
nativeTail = nativeTail.slice(start);
|
|
385
|
+
if (nativeTail.length > MAX_NATIVE_LINE_CHARS) {
|
|
386
|
+
// No newline within the cap: drop the line and warn. On the final flush there is no
|
|
387
|
+
// remainder to skip, so only the buffer is cleared.
|
|
388
|
+
nativeTail = "";
|
|
389
|
+
if (!final) {
|
|
390
|
+
nativeOverflow = true;
|
|
391
|
+
opts.onAgentWarning?.(`native output line exceeded ${MAX_NATIVE_LINE_CHARS / (1024 * 1024)} MiB without a newline and was discarded`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else if (final && nativeTail !== "") {
|
|
395
|
+
emitNativeLine(nativeTail);
|
|
396
|
+
nativeTail = "";
|
|
397
|
+
}
|
|
365
398
|
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
399
|
+
child.stdout?.on("data", (chunk) => {
|
|
400
|
+
wd.noteByte();
|
|
401
|
+
feedCanonical(decoder.write(chunk));
|
|
402
|
+
});
|
|
403
|
+
child.stderr?.on("data", (chunk) => {
|
|
404
|
+
// stderr is rare and meaningful (crash traces, warnings): treat it as progress.
|
|
405
|
+
wd.noteStderr();
|
|
406
|
+
const text = stderrDecoder.write(chunk);
|
|
407
|
+
fallbackClone ??= backend.matchStderrWarning?.(text, stderrTail);
|
|
408
|
+
stderrTail = (stderrTail + text).slice(-STDERR_MATCH_TAIL);
|
|
409
|
+
stderr += text;
|
|
410
|
+
if (stderr.length > 64 * 1024)
|
|
411
|
+
stderr = stderr.slice(-64 * 1024);
|
|
412
|
+
});
|
|
413
|
+
const finish = (result) => {
|
|
414
|
+
if (settled)
|
|
415
|
+
return;
|
|
416
|
+
settled = true;
|
|
417
|
+
// A blind image read (plans/image-input.md "Detecting a blind read"): surface it to the
|
|
418
|
+
// caller once, at settle, with the action its kind earned — the authoring run was killed
|
|
419
|
+
// ("fail"), a gate run only recorded it ("logged"). The caller logs the image_blind event
|
|
420
|
+
// with its own role/run context.
|
|
421
|
+
if (parser.imageBlind) {
|
|
422
|
+
opts.onBlindRead?.(parser.imageBlind.path, opts.kind === "author" ? "fail" : "logged");
|
|
371
423
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Every PiRunResult is built here from what the parser has seen plus how the run ended,
|
|
377
|
-
// so adding a field to PiRunResult touches this single place. The spawn-error path passes
|
|
378
|
-
// only an errorMessage override: at that point no output ever arrived, so the parser holds
|
|
379
|
-
// exactly its initial values — which is precisely what a failed spawn reports.
|
|
380
|
-
const resultFromParser = (overrides) => ({
|
|
381
|
-
ok: false,
|
|
382
|
-
finalText: parser.finalText,
|
|
383
|
-
nothingToDo: parser.declaredNothingToDo,
|
|
384
|
-
refused: parser.refused,
|
|
385
|
-
refusedReason: parser.refusedReason || undefined,
|
|
386
|
-
verdictText: parser.verdictText || undefined,
|
|
387
|
-
outputTokens: parser.outputTokens,
|
|
388
|
-
promptTokens: parser.promptTokens,
|
|
389
|
-
cacheReadTokens: parser.cacheReadTokens,
|
|
390
|
-
preEditPromptTokens: parser.preEditPromptTokens,
|
|
391
|
-
firstEditTurn: parser.firstEditTurn,
|
|
392
|
-
peakContextTokens: parser.peakContextTokens,
|
|
393
|
-
turns: parser.turns,
|
|
394
|
-
costUsd: parser.costUsd,
|
|
395
|
-
stopReason: parser.stopReason,
|
|
396
|
-
errorMessage: undefined,
|
|
397
|
-
timedOut: false,
|
|
398
|
-
timedOutProgressing: false,
|
|
399
|
-
quietKilled: false,
|
|
400
|
-
aborted,
|
|
401
|
-
contextExceeded: parser.contextExceeded,
|
|
402
|
-
transientServerTimeout: parser.transientServerTimeout,
|
|
403
|
-
transientRateLimit: parser.transientRateLimit,
|
|
404
|
-
transientBackend: parser.transientBackend,
|
|
405
|
-
backendKind: parser.backendFailureKind,
|
|
406
|
-
configError: parser.configError,
|
|
407
|
-
retryAfterSeconds: parser.retryAfterSeconds,
|
|
408
|
-
transientPiCrash: false,
|
|
409
|
-
fallbackClone,
|
|
410
|
-
finalMessageContentless: parser.finalMessageContentless,
|
|
411
|
-
producedAssistantContent: parser.producedAssistantContent,
|
|
412
|
-
compacted: parser.compacted,
|
|
413
|
-
budgetCapped: parser.budgetCapped,
|
|
414
|
-
resumeMissing: parser.resumeMissing,
|
|
415
|
-
billing: parser.billing,
|
|
416
|
-
rateWindows: parser.rateWindows.length > 0 ? parser.rateWindows : undefined,
|
|
417
|
-
usedOverage: parser.usedOverage,
|
|
418
|
-
...overrides,
|
|
419
|
-
});
|
|
420
|
-
child.on("error", (err) => {
|
|
421
|
-
finish(resultFromParser({
|
|
422
|
-
errorMessage: spawnErrorMessage(resolved, err.message, backend),
|
|
423
|
-
}));
|
|
424
|
-
});
|
|
425
|
-
// Sweep the run however it ended, a normal exit included, in two passes:
|
|
426
|
-
//
|
|
427
|
-
// 1. The group pi led: pi exits before its orphans — a tool call's `(server &)` or
|
|
428
|
-
// `cd … && server &` is reparented to PID 1 but stays in pi's group — and the harness
|
|
429
|
-
// cannot rely on the model's own cleanup (BUGS.md 2026-09-23: qa's unauthenticated
|
|
430
|
-
// `gui --all-interfaces` listened on the LAN for 7.5 hours after a tick that ended
|
|
431
|
-
// "Everything checked out"). On 'exit', not 'close': pi has just been reaped, so the
|
|
432
|
-
// pgid still names only its group (never reissued while a member lives), and an orphan
|
|
433
|
-
// holding pi's stdio open dies now instead of holding 'close' — and the run — open.
|
|
434
|
-
// Synchronous, so it adds nothing to the run's resolution; on the kill paths it
|
|
435
|
-
// re-sends a SIGTERM the group already had.
|
|
436
|
-
//
|
|
437
|
-
// 2. The marker sweep: pi 0.87.1's bash tool starts every command detached, in its own
|
|
438
|
-
// process group, so whatever a tool call backgrounds sits in a group whose leader is
|
|
439
|
-
// not pi and pass 1 never reaches it (BUGS.md 2026-09-30 — leaked servers and test
|
|
440
|
-
// workers outlived their ticks by hours). Every process the run started carries the
|
|
441
|
-
// run's TUMWATER_RUN mark in its environment, so the same-user scan finds them
|
|
442
|
-
// wherever they were reparented. Fire-and-forget: one process-table scan, never
|
|
443
|
-
// awaited — the run resolves on pi's output, and the sweep's own SIGKILL escalation
|
|
444
|
-
// (10 s) is unref'd like signalTree's.
|
|
445
|
-
child.on("exit", () => {
|
|
446
|
-
terminateChild(child);
|
|
447
|
-
void sweepRunMarker(runMarker).catch(() => { });
|
|
448
|
-
});
|
|
449
|
-
child.on("close", (code) => {
|
|
450
|
-
// Flush any bytes the decoder held back at stream end and any unterminated final line
|
|
451
|
-
// so the tail is not lost, then let the translator flush its own pending canonical
|
|
452
|
-
// line(s) — a batching backend's unclosed last assistant message, which a killed run
|
|
453
|
-
// never closes with a later event.
|
|
454
|
-
feedCanonical(decoder.end(), true);
|
|
455
|
-
for (const canonical of translator.finalize?.() ?? [])
|
|
456
|
-
parser.feed(canonical + "\n", onLine);
|
|
457
|
-
// A blind image read on an authoring run deliberately killed it (terminateRun above),
|
|
458
|
-
// so its exit is a kill like the watchdogs' — the half-done work is discarded.
|
|
459
|
-
const killed = aborted || wd.timedOut || wd.quietKilled || blindRead !== undefined;
|
|
460
|
-
// The backend's post-run reconciliation (opencode's authoritative session export), only
|
|
461
|
-
// when the run is not already settled (a failed spawn settles in the 'error' handler
|
|
462
|
-
// before 'close') and was not killed — a killed run's session is half-done and no export
|
|
463
|
-
// should spend its 10 s on it. Its lines are written and parsed like streamed ones, so
|
|
464
|
-
// they reach the raw log and the parser's authoritative fold (run_cost/run_usage).
|
|
465
|
-
if (!settled && !killed) {
|
|
466
|
-
for (const canonical of backend.afterRun?.(runSpec, invocation, translator) ?? []) {
|
|
467
|
-
parser.feed(canonical + "\n", onLine);
|
|
424
|
+
// Best-effort: an unremovable scratch dir (read-only subdir, a straggler still writing) must
|
|
425
|
+
// not throw out of the close handler before the run's result resolves.
|
|
426
|
+
try {
|
|
427
|
+
removeTree(runTmp);
|
|
468
428
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
429
|
+
catch {
|
|
430
|
+
/* left for the OS temp sweep */
|
|
431
|
+
}
|
|
432
|
+
wd.stop();
|
|
433
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
434
|
+
// Surface the translator's per-run warnings once, after all native lines were fed.
|
|
435
|
+
for (const warning of translator.warnings?.() ?? [])
|
|
436
|
+
opts.onAgentWarning?.(warning);
|
|
437
|
+
// Resolve only once the logs have flushed: writes complete on libuv's threadpool, so
|
|
438
|
+
// resolving before 'finish' can leave the tail of <role>.pi.jsonl (or the native log)
|
|
439
|
+
// unwritten — a run that dies right after runAgent settles (an abort during a restart
|
|
440
|
+
// drain, a supervisor swap) loses its last lines. end()'s callback fires on 'finish';
|
|
441
|
+
// if a stream errors instead it never does, so settle from 'error' too — a broken log
|
|
442
|
+
// degrades to a lost log, never a stuck run.
|
|
443
|
+
let resolved = false;
|
|
444
|
+
const settle = () => {
|
|
445
|
+
if (!resolved) {
|
|
446
|
+
resolved = true;
|
|
447
|
+
resolve(result);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
let pending = 0;
|
|
451
|
+
const flushed = () => {
|
|
452
|
+
pending -= 1;
|
|
453
|
+
if (pending === 0)
|
|
454
|
+
settle();
|
|
455
|
+
};
|
|
456
|
+
for (const log of [rawLog, nativeLog]) {
|
|
457
|
+
log.stream.on("error", settle);
|
|
458
|
+
if (!log.broken) {
|
|
459
|
+
pending += 1;
|
|
460
|
+
log.stream.end(flushed);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (pending === 0)
|
|
464
|
+
settle();
|
|
465
|
+
};
|
|
466
|
+
// The run state the settlement-half handlers (run-settle.ts) read when their event
|
|
467
|
+
// fires. The getters read through to the run's still-mutating feed state, so a handler
|
|
468
|
+
// sees the current value rather than a snapshot taken when its listener was registered.
|
|
469
|
+
const state = {
|
|
470
|
+
child,
|
|
471
|
+
resolved,
|
|
472
|
+
backend,
|
|
473
|
+
runMarker,
|
|
474
|
+
parser,
|
|
475
|
+
translator,
|
|
476
|
+
decoder,
|
|
477
|
+
wd,
|
|
478
|
+
finish,
|
|
479
|
+
feedCanonical,
|
|
480
|
+
onLine,
|
|
481
|
+
runSpec,
|
|
482
|
+
invocation,
|
|
483
|
+
stderr: () => stderr,
|
|
484
|
+
fallbackClone: () => fallbackClone,
|
|
485
|
+
settled: () => settled,
|
|
486
|
+
blindRead: () => blindRead,
|
|
487
|
+
aborted: () => aborted,
|
|
488
|
+
wasKilled: () => wasKilled(),
|
|
489
|
+
kind: opts.kind,
|
|
490
|
+
config: opts.config,
|
|
491
|
+
onLeftoversSwept: opts.onLeftoversSwept,
|
|
492
|
+
};
|
|
493
|
+
child.on("error", (err) => {
|
|
494
|
+
settleError(state, err);
|
|
495
|
+
});
|
|
496
|
+
// Sweep the run however it ended, a normal exit included, in two passes:
|
|
497
|
+
//
|
|
498
|
+
// 1. The group pi led: pi exits before its orphans — a tool call's `(server &)` or
|
|
499
|
+
// `cd … && server &` is reparented to PID 1 but stays in pi's group — and the harness
|
|
500
|
+
// cannot rely on the model's own cleanup (BUGS.md 2026-09-23: qa's unauthenticated
|
|
501
|
+
// `gui --all-interfaces` listened on the LAN for 7.5 hours after a run that ended
|
|
502
|
+
// "Everything checked out"). On 'exit', not 'close': pi has just been reaped, so the
|
|
503
|
+
// pgid still names only its group (never reissued while a member lives), and an orphan
|
|
504
|
+
// holding pi's stdio open dies now instead of holding 'close' — and the run — open.
|
|
505
|
+
// Synchronous, so it adds nothing to the run's resolution; on the kill paths it
|
|
506
|
+
// re-sends a SIGTERM the group already had.
|
|
507
|
+
//
|
|
508
|
+
// 2. The marker sweep: pi 0.87.1's bash tool starts every command detached, in its own
|
|
509
|
+
// process group, so whatever a tool call backgrounds sits in a group whose leader is
|
|
510
|
+
// not pi and pass 1 never reaches it (BUGS.md 2026-09-30 — leaked servers and test
|
|
511
|
+
// workers outlived their runs by hours). Every process the run started carries the
|
|
512
|
+
// run's TUMWATER_RUN mark in its environment, so the same-user scan finds them
|
|
513
|
+
// wherever they were reparented. Fire-and-forget: one process-table scan, never
|
|
514
|
+
// awaited — the run resolves on pi's output, and the sweep's own SIGKILL escalation
|
|
515
|
+
// (10 s) is unref'd like signalTree's.
|
|
516
|
+
child.on("exit", () => {
|
|
517
|
+
settleExit(state);
|
|
518
|
+
});
|
|
519
|
+
child.on("close", (code) => {
|
|
520
|
+
settleClose(state, code);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
// A synchronous throw in the run's build-and-spawn stage (claude's invocation rejects a
|
|
525
|
+
// run with no resolved model) rejects the run, preserving the contract a synchronous
|
|
526
|
+
// executor's throw used to get from the Promise constructor.
|
|
527
|
+
reject(err instanceof Error ? err : new Error(`runAgent: ${String(err)}`));
|
|
528
|
+
}
|
|
512
529
|
});
|
|
513
530
|
}
|
|
514
531
|
/** The historical name for the neutral runner: every existing call site, fake, and test keeps
|