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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** List the paths an `apply_patch` text writes, from the patch text alone (plans/
|
|
2
|
+
* codex-write-guard.md "The hook (parts 1–2)"; PLANS.md "Write guard on Codex, part 1/9").
|
|
3
|
+
* Pure: no filesystem, no imports beyond node built-ins, so a later part can call it from the
|
|
4
|
+
* hook without dragging in a registry or subprocess.
|
|
5
|
+
*
|
|
6
|
+
* Codex's PreToolUse payload carries the patch text, not a path, and a patch is applied even
|
|
7
|
+
* when it is truncated or its `*** Begin Patch` / `*** End Patch` markers are missing — so this
|
|
8
|
+
* scans every line, marker or not, and trusts only lines that begin at column 0 with a header.
|
|
9
|
+
* A hunk line (`+`, `-`, a space, `@@`) never counts as a header even when its text reads like
|
|
10
|
+
* one, because a hunk prefix shifts `*** ` off column 0. Paths are kept as written; the caller
|
|
11
|
+
* resolves relative ones against the run's directory. */
|
|
12
|
+
/** Headers that name a path the patch writes, in the order a line may spell them. */
|
|
13
|
+
const HEADERS = [
|
|
14
|
+
"*** Add File: ",
|
|
15
|
+
"*** Delete File: ",
|
|
16
|
+
"*** Update File: ",
|
|
17
|
+
"*** Move to: ",
|
|
18
|
+
];
|
|
19
|
+
/** The paths an `apply_patch` text writes, in first-appearance order, without duplicates. */
|
|
20
|
+
export function patchWritePaths(patch) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const paths = [];
|
|
23
|
+
for (const line of patch.split("\n")) {
|
|
24
|
+
for (const header of HEADERS) {
|
|
25
|
+
if (line.startsWith(header)) {
|
|
26
|
+
const path = line.slice(header.length).replace(/[\s\r]+$/, "");
|
|
27
|
+
if (path !== "" && !seen.has(path)) {
|
|
28
|
+
seen.add(path);
|
|
29
|
+
paths.push(path);
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return paths;
|
|
36
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/** The codex adapter's rollout reader (plans/codex-backend.md "The rollout read that
|
|
2
|
+
* completes it"). `codex exec --json`'s stream reports usage only once, at the end, counting
|
|
3
|
+
* the WHOLE thread (finding 4) — a resume would otherwise charge its earlier run to the follow-
|
|
4
|
+
* up. The authoritative per-run figures live in `$CODEX_HOME/sessions/…/rollout-*-<id>.jsonl`,
|
|
5
|
+
* Codex's own JSONL record of a session, where each run's `task_started` .. `task_complete`
|
|
6
|
+
* block carries its own `token_usage_record`, `token_count` (the rate limits) and
|
|
7
|
+
* `turn_context` (the model that actually ran). This module reads just that block: what the
|
|
8
|
+
* stream lacks — this run's own usage, the billing and the two usage windows.
|
|
9
|
+
*
|
|
10
|
+
* Pure over text (plus one filesystem walk for `findRollout`): no spawn, no billing math, no
|
|
11
|
+
* session state. Part 7/9's adapter wires the lines into the canonical stream in `afterRun`.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { isJsonObject, nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
16
|
+
import { windowTypeName } from "./codex-stream.js";
|
|
17
|
+
function str(value) {
|
|
18
|
+
return typeof value === "string" ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function finite(value) {
|
|
21
|
+
return nonNegativeNumber(value, undefined);
|
|
22
|
+
}
|
|
23
|
+
/** The rate-limit status of one window: `rejected` when the account hit it outright,
|
|
24
|
+
* `allowed_warning` when it is at or past 90% full, else `allowed` (design §5, decision 7). */
|
|
25
|
+
function windowStatus(windowType, rateLimitReachedType, usedPercent) {
|
|
26
|
+
if (rateLimitReachedType === windowType)
|
|
27
|
+
return "rejected";
|
|
28
|
+
if (usedPercent !== undefined && usedPercent >= 100)
|
|
29
|
+
return "rejected";
|
|
30
|
+
if (usedPercent !== undefined && usedPercent >= 90)
|
|
31
|
+
return "allowed_warning";
|
|
32
|
+
return "allowed";
|
|
33
|
+
}
|
|
34
|
+
/** Read the records of ONE turn out of a rollout's text. A rollout is one file per thread that
|
|
35
|
+
* appends a new `task_started` block per run, so the adapter's `afterRun` read (which knows
|
|
36
|
+
* only the thread id that names the file, never a turn id) reads the LAST block — everything
|
|
37
|
+
* after the last `task_started`. `sinceTurnStart` (optional) selects a specific turn's block
|
|
38
|
+
* instead, which pins per-turn isolation: the thread total hides a resume's own figures
|
|
39
|
+
* (finding 4). `requestedModel` is the `-m` tumwater passed; a turn whose `turn_context.model`
|
|
40
|
+
* differs reports the switch. */
|
|
41
|
+
export function rolloutLines(text, opts) {
|
|
42
|
+
const { sinceTurnStart, requestedModel } = opts;
|
|
43
|
+
let currentTurn;
|
|
44
|
+
let sawTurn = false;
|
|
45
|
+
let lastUsage;
|
|
46
|
+
let lastRateLimits;
|
|
47
|
+
let model;
|
|
48
|
+
let planType;
|
|
49
|
+
for (const line of text.split("\n")) {
|
|
50
|
+
if (!line.trim())
|
|
51
|
+
continue;
|
|
52
|
+
const parsed = parseJsonObject(line);
|
|
53
|
+
if (!parsed)
|
|
54
|
+
continue; // a torn or foreign line belongs to no turn
|
|
55
|
+
const payload = objectOrEmpty(parsed.payload);
|
|
56
|
+
// `task_started`, `task_complete` and `token_count` come wrapped in an `event_msg` whose
|
|
57
|
+
// `payload.type` discriminates; `turn_context` and `token_usage_record` name themselves in
|
|
58
|
+
// the outer `type`. Read either.
|
|
59
|
+
const ptype = str(parsed.type) === "event_msg" ? str(payload.type) : str(parsed.type);
|
|
60
|
+
if (ptype === "task_started") {
|
|
61
|
+
currentTurn = str(payload.turn_id);
|
|
62
|
+
// Each new task_started begins a fresh turn (a resume appends a second run to the same
|
|
63
|
+
// file), so the collectors restart at the block we are reading. With no turn id (the
|
|
64
|
+
// adapter's read) every task_started restarts them and the last block wins; with one,
|
|
65
|
+
// only the requested turn's does.
|
|
66
|
+
if (sinceTurnStart === undefined || currentTurn === sinceTurnStart) {
|
|
67
|
+
sawTurn = true;
|
|
68
|
+
lastUsage = undefined;
|
|
69
|
+
lastRateLimits = undefined;
|
|
70
|
+
model = undefined;
|
|
71
|
+
planType = undefined;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (sinceTurnStart !== undefined && currentTurn !== sinceTurnStart)
|
|
76
|
+
continue;
|
|
77
|
+
switch (ptype) {
|
|
78
|
+
case "turn_context":
|
|
79
|
+
if (model === undefined)
|
|
80
|
+
model = str(payload.model);
|
|
81
|
+
break;
|
|
82
|
+
case "token_usage_record": {
|
|
83
|
+
const turnUsage = objectOrEmpty(payload.turn_token_usage);
|
|
84
|
+
if (isJsonObject(turnUsage) && (turnUsage.input_tokens !== undefined || turnUsage.output_tokens !== undefined)) {
|
|
85
|
+
lastUsage = turnUsage;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "token_count": {
|
|
90
|
+
const rateLimits = objectOrEmpty(payload.rate_limits);
|
|
91
|
+
lastRateLimits = rateLimits;
|
|
92
|
+
const plan = str(rateLimits.plan_type);
|
|
93
|
+
if (plan !== undefined)
|
|
94
|
+
planType = plan;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
default:
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// No matching task_started, or the turn's usage record is missing or malformed (a truncated
|
|
102
|
+
// or torn file): there is no reading. (currentTurn will have moved on to a later run's
|
|
103
|
+
// task_started in a resume-shared file; sawTurn — not currentTurn — is the real signal.)
|
|
104
|
+
if (!sawTurn || !isJsonObject(lastUsage)) {
|
|
105
|
+
return { lines: [] };
|
|
106
|
+
}
|
|
107
|
+
const lines = [];
|
|
108
|
+
const input = finite(lastUsage.input_tokens) ?? 0;
|
|
109
|
+
const cacheRead = finite(lastUsage.cached_input_tokens) ?? 0;
|
|
110
|
+
lines.push(JSON.stringify({
|
|
111
|
+
type: "run_usage",
|
|
112
|
+
input: Math.max(0, input - cacheRead),
|
|
113
|
+
output: finite(lastUsage.output_tokens) ?? 0,
|
|
114
|
+
cacheRead,
|
|
115
|
+
cacheWrite: finite(lastUsage.cache_write_input_tokens) ?? 0,
|
|
116
|
+
}));
|
|
117
|
+
if (planType !== undefined) {
|
|
118
|
+
// A reading with a plan_type bills on the subscription (finding 5; API-key login is
|
|
119
|
+
// refused, decision 5).
|
|
120
|
+
lines.push(JSON.stringify({ type: "billing", billing: "subscription" }));
|
|
121
|
+
}
|
|
122
|
+
// Two windows when present: the primary (5 h) and the secondary (weekly). A killed run's
|
|
123
|
+
// rollout has a token_usage_record but no token_count, so it yields run_usage with no
|
|
124
|
+
// rate_limit (finding 23).
|
|
125
|
+
if (lastRateLimits !== undefined) {
|
|
126
|
+
const reached = str(lastRateLimits.rate_limit_reached_type);
|
|
127
|
+
for (const key of ["primary", "secondary"]) {
|
|
128
|
+
const windowRaw = lastRateLimits[key];
|
|
129
|
+
if (!isJsonObject(windowRaw))
|
|
130
|
+
continue;
|
|
131
|
+
const minutes = finite(windowRaw.window_minutes);
|
|
132
|
+
if (minutes === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
const typeName = windowTypeName(minutes);
|
|
135
|
+
const usedPercent = finite(windowRaw.used_percent);
|
|
136
|
+
lines.push(JSON.stringify({
|
|
137
|
+
type: "rate_limit",
|
|
138
|
+
rateLimitType: typeName,
|
|
139
|
+
status: windowStatus(typeName, reached, usedPercent),
|
|
140
|
+
...(usedPercent !== undefined ? { utilization: usedPercent / 100 } : {}),
|
|
141
|
+
...(finite(windowRaw.resets_at) !== undefined ? { resetsAt: finite(windowRaw.resets_at) } : {}),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const switchedTo = model !== undefined && requestedModel !== undefined && model !== requestedModel ? model : undefined;
|
|
146
|
+
return { lines, ...(switchedTo !== undefined ? { switchedTo } : {}) };
|
|
147
|
+
}
|
|
148
|
+
/** Locate the rollout file of `$CODEX_HOME` that names the session `id`: the first
|
|
149
|
+
* `sessions/…/rollout-*-<id>.jsonl` under `home`, or null. It takes no start date and does no
|
|
150
|
+
* date math — the id match alone decides (finding 5: Codex's rollout name carries local time,
|
|
151
|
+
* so a UTC-derived bound can miss the file; the sessions tree is one directory per active day,
|
|
152
|
+
* so the walk stays small). */
|
|
153
|
+
export function findRollout(home, id) {
|
|
154
|
+
const sessionsRoot = path.join(home, "sessions");
|
|
155
|
+
if (!fs.existsSync(sessionsRoot))
|
|
156
|
+
return null;
|
|
157
|
+
const suffix = `-${id}.jsonl`;
|
|
158
|
+
const stack = [sessionsRoot];
|
|
159
|
+
while (stack.length > 0) {
|
|
160
|
+
const dir = stack.pop();
|
|
161
|
+
let entries;
|
|
162
|
+
try {
|
|
163
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue; // an unreadable dir is simply no match, never a failure
|
|
167
|
+
}
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
const full = path.join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
stack.push(full);
|
|
172
|
+
}
|
|
173
|
+
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
|
|
174
|
+
return full;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/** The codex adapter's native-to-canonical stream translator (plans/codex-backend.md "The
|
|
2
|
+
* stream translator and the rollout read that completes it"). `codex exec --json` emits one
|
|
3
|
+
* JSON object per line whose vocabulary is nothing like pi's: `thread.started`, then item
|
|
4
|
+
* start/completed events (`agent_message`, `command_execution`, `file_change`, `error`), and
|
|
5
|
+
* finally `turn.completed` or `turn.failed`. Rather than teach ~fifteen readers a third
|
|
6
|
+
* dialect, every native line is translated here into pi's vocabulary — the canonical stream —
|
|
7
|
+
* so `PiStreamParser`, the raw log, the transcript, progress, and the GUI all stay unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
|
|
10
|
+
* state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
|
|
11
|
+
* call it once per run. Unmapped native types, the distinct text of `error` items, and a
|
|
12
|
+
* command abandoned at the end of its turn are recorded once each on the seam's optional
|
|
13
|
+
* `warnings()` — src/agent/run-agent.ts forwards each as an `agent_unknown_event` warning
|
|
14
|
+
* after the run. `sessionId()` reports the thread id once `thread.started` has named it.
|
|
15
|
+
*/
|
|
16
|
+
import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
17
|
+
import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
|
|
18
|
+
import { secondsUntil } from "../../text/datetime.js";
|
|
19
|
+
/** How codex wraps every shell command — the prefix to strip (with its quoting) so the
|
|
20
|
+
* canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
|
|
21
|
+
const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
|
|
22
|
+
/** A top-level `error` line that is Codex's own WebSocket/HTTPS reconnect ("Reconnecting...
|
|
23
|
+
* n/m (reason)") — canonical `auto_retry_start`. */
|
|
24
|
+
const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
|
|
25
|
+
/** The item types dropped silently, with no canonical event and no `warnings()` entry
|
|
26
|
+
* (design §5's `item.*` row): `todo_list` and `web_search`. */
|
|
27
|
+
const DROPPED_ITEM_TYPES = new Set(["todo_list", "web_search"]);
|
|
28
|
+
/** HTTP status classes that mean a transient backend failure (shared with the claude and
|
|
29
|
+
* codex classifiers' reading of `codex exec`'s JSON error). */
|
|
30
|
+
const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
31
|
+
/** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
|
|
32
|
+
* the model's own command — the argument suite-rerun detection matches. Passes a command the
|
|
33
|
+
* capture did not wrap through unchanged. */
|
|
34
|
+
function innerCommand(command) {
|
|
35
|
+
const m = SHELL_WRAPPER.exec(command);
|
|
36
|
+
return m ? m[1] : command;
|
|
37
|
+
}
|
|
38
|
+
/** The canonical window type name for a window-minutes value (shared with the rollout reader
|
|
39
|
+
* in codex-rollout.ts): Codex's 300-minute window reads as `five_hour`, its 10080-minute as
|
|
40
|
+
* `seven_day`, so a reserve reads the same on claude and codex (decision 7). */
|
|
41
|
+
export function windowTypeName(windowMinutes) {
|
|
42
|
+
if (windowMinutes === 300)
|
|
43
|
+
return "five_hour";
|
|
44
|
+
if (windowMinutes === 10080)
|
|
45
|
+
return "seven_day";
|
|
46
|
+
return `${windowMinutes}_minute`;
|
|
47
|
+
}
|
|
48
|
+
/** One codex `error` item's distinct message → a run warning; repeated identical messages are
|
|
49
|
+
* warned once (finding 11). */
|
|
50
|
+
/** How a `turn.failed` error message classifies into the canonical `tumwaterKind` (and, for a
|
|
51
|
+
* transient backend, its kind), trusted by `PiStreamParser` before its text regexes. The inner
|
|
52
|
+
* `message` is a JSON string carrying `status` and `error.type` / `error.code` (finding 11).
|
|
53
|
+
* Returns the canonical lines to emit beside the closing `message_end`: a rejected rate-limit
|
|
54
|
+
* reading when the error names a used-up window. */
|
|
55
|
+
function classifyTurnFailed(parsed) {
|
|
56
|
+
const kind = {};
|
|
57
|
+
const error = objectOrEmpty(parsed.error);
|
|
58
|
+
const code = typeof error.code === "string" ? error.code : undefined;
|
|
59
|
+
const errType = typeof error.type === "string" ? error.type : undefined;
|
|
60
|
+
const message = typeof error.message === "string" ? error.message : typeof parsed.message === "string" ? parsed.message : undefined;
|
|
61
|
+
kind.errorMessage = message;
|
|
62
|
+
const status = nonNegativeNumber(parsed.status, undefined);
|
|
63
|
+
// The context-window signal is tested BEFORE the generic 400 `invalid_request_error`
|
|
64
|
+
// branch (as claude's classifyResult tests "prompt is too long" before CONFIG_STATUS): a
|
|
65
|
+
// `context_window_exceeded` code, or a 400 whose message names the context window, is
|
|
66
|
+
// `contextExceeded`, never `configError`.
|
|
67
|
+
const namesContextWindow = (t) => t !== undefined &&
|
|
68
|
+
/context window|context length|too long|exceeds? (the )?(model'?s )?context|prompt is too long/i.test(t);
|
|
69
|
+
if (code === "context_window_exceeded" || (status === 400 && errType === "invalid_request_error" && namesContextWindow(message))) {
|
|
70
|
+
kind.kind = "contextExceeded";
|
|
71
|
+
return kind;
|
|
72
|
+
}
|
|
73
|
+
if (code === "usage_limit_reached" || code === "rate_limit_exceeded" || status === 429) {
|
|
74
|
+
// Which window the account hit, from the error's own `rate_limit_reached_type` (else the
|
|
75
|
+
// five-hour window, the one Plus exhausts first). The rejected reading's wait comes from
|
|
76
|
+
// `resets_at`.
|
|
77
|
+
const minutes = nonNegativeNumber(parsed.rate_limit_reached_type, undefined);
|
|
78
|
+
const named = typeof parsed.rate_limit_reached_type === "string" ? parsed.rate_limit_reached_type : undefined;
|
|
79
|
+
const resetsAt = nonNegativeNumber(parsed.resets_at, undefined);
|
|
80
|
+
// secondsUntil folds the unix-seconds resets stamp (×1000 to epoch-ms), clamped at 0.
|
|
81
|
+
const retryAfter = resetsAt === undefined ? undefined : secondsUntil(resetsAt * 1000, Date.now());
|
|
82
|
+
kind.kind = "transientRateLimit";
|
|
83
|
+
kind.rateLimit = {
|
|
84
|
+
type: "rate_limit",
|
|
85
|
+
rateLimitType: typeof named === "string" && /_minute$|_hour$|_day$/.test(named)
|
|
86
|
+
? named
|
|
87
|
+
: windowTypeName(minutes ?? 300),
|
|
88
|
+
status: "rejected",
|
|
89
|
+
utilization: 1,
|
|
90
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
91
|
+
...(retryAfter !== undefined ? { retryAfterSeconds: retryAfter } : {}),
|
|
92
|
+
};
|
|
93
|
+
return kind;
|
|
94
|
+
}
|
|
95
|
+
if (code === "usage_not_included") {
|
|
96
|
+
kind.kind = "configError";
|
|
97
|
+
return kind;
|
|
98
|
+
}
|
|
99
|
+
if (status !== undefined && SERVER_STATUS.has(status)) {
|
|
100
|
+
kind.kind = "transientBackend";
|
|
101
|
+
kind.backendKind = "server";
|
|
102
|
+
return kind;
|
|
103
|
+
}
|
|
104
|
+
// A refresh-token failure and an absent/invalid login are permanent config errors, which
|
|
105
|
+
// the doctor names with the login command (design §5).
|
|
106
|
+
if (/refresh_token_reused|invalidated refresh token/i.test(message ?? "")) {
|
|
107
|
+
kind.kind = "configError";
|
|
108
|
+
return kind;
|
|
109
|
+
}
|
|
110
|
+
if (status === 401) {
|
|
111
|
+
kind.kind = "configError";
|
|
112
|
+
return kind;
|
|
113
|
+
}
|
|
114
|
+
if (status === 400 && errType === "invalid_request_error") {
|
|
115
|
+
// A 400 `invalid_request_error` names the model as unsupported on a ChatGPT account.
|
|
116
|
+
kind.kind = "configError";
|
|
117
|
+
return kind;
|
|
118
|
+
}
|
|
119
|
+
return kind;
|
|
120
|
+
}
|
|
121
|
+
/** Build a fresh per-run codex translator. `cwd` (optional) lands on the canonical `session`
|
|
122
|
+
* event (design §5's `session {id, cwd}`) so part 7/9's adapter can stamp the run's own
|
|
123
|
+
* directory; when omitted the session carries no `cwd`, like the opencode translator. */
|
|
124
|
+
export function codexTranslator(opts = {}) {
|
|
125
|
+
const bashLimit = outputLimits(opts.contextWindow).bash;
|
|
126
|
+
const cwd = opts.cwd;
|
|
127
|
+
let sessionIdValue;
|
|
128
|
+
let sawTurnCompleted = false;
|
|
129
|
+
let sawTurnFailed = false;
|
|
130
|
+
/** Tool calls open on this run, keyed by the item id that started them: command_execution
|
|
131
|
+
* and file_change items. A call still open when the turn ends was abandoned (finding 19).
|
|
132
|
+
* The value is the item type, so the abandonment close can name a command vs an edit. */
|
|
133
|
+
const openCalls = new Map();
|
|
134
|
+
let warnedAbandoned = false;
|
|
135
|
+
/** The last non-reconnecting top-level `error` message, held as the fallback text for a
|
|
136
|
+
* following `turn.failed` whose own message is absent (design §5). */
|
|
137
|
+
let pendingFailureText;
|
|
138
|
+
let turnUsage;
|
|
139
|
+
const seenEvents = new Set();
|
|
140
|
+
const seenItems = new Set();
|
|
141
|
+
const warnings = new Set();
|
|
142
|
+
const recordEvent = (type) => {
|
|
143
|
+
if (seenEvents.has(type))
|
|
144
|
+
return;
|
|
145
|
+
seenEvents.add(type);
|
|
146
|
+
warnings.add(type);
|
|
147
|
+
};
|
|
148
|
+
const recordItem = (item) => {
|
|
149
|
+
const key = `item:${typeof item.type === "string" ? item.type : "unknown"}`;
|
|
150
|
+
if (seenItems.has(key))
|
|
151
|
+
return;
|
|
152
|
+
seenItems.add(key);
|
|
153
|
+
warnings.add(key);
|
|
154
|
+
};
|
|
155
|
+
/** Record one distinct `error` item message as a run warning (never a failure). */
|
|
156
|
+
const recordErrorItem = (message) => {
|
|
157
|
+
if (warnings.has(message))
|
|
158
|
+
return;
|
|
159
|
+
warnings.add(message);
|
|
160
|
+
};
|
|
161
|
+
/** Close every still-open tool call as abandoned (the turn ended while they ran, finding
|
|
162
|
+
* 19): the command's open `sleep` never completed. Returns the canonical close lines.
|
|
163
|
+
* The `codex_command_abandoned` warning is recorded only when an open `command_execution`
|
|
164
|
+
* was actually closed (design §5), so a clean run's `warnings()` stays empty and a
|
|
165
|
+
* file_change abandoned on its own does not claim a command was. */
|
|
166
|
+
const abandonOpenCalls = () => {
|
|
167
|
+
const out = [];
|
|
168
|
+
let abandonedCommand = false;
|
|
169
|
+
for (const [id, type] of openCalls) {
|
|
170
|
+
openCalls.delete(id);
|
|
171
|
+
if (type === "command_execution")
|
|
172
|
+
abandonedCommand = true;
|
|
173
|
+
out.push(JSON.stringify({
|
|
174
|
+
type: "tool_execution_end",
|
|
175
|
+
toolCallId: id,
|
|
176
|
+
isError: true,
|
|
177
|
+
...(type === "command_execution"
|
|
178
|
+
? { result: "abandoned: the run ended while this command was still running" }
|
|
179
|
+
: {}),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
if (abandonedCommand && !warnedAbandoned) {
|
|
183
|
+
warnedAbandoned = true;
|
|
184
|
+
warnings.add("codex_command_abandoned");
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
};
|
|
188
|
+
const translate = (line) => {
|
|
189
|
+
const parsed = parseJsonObject(line);
|
|
190
|
+
if (!parsed)
|
|
191
|
+
return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
|
|
192
|
+
const event = parsed;
|
|
193
|
+
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
194
|
+
const out = [];
|
|
195
|
+
switch (type) {
|
|
196
|
+
case "thread.started": {
|
|
197
|
+
const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
|
|
198
|
+
if (id === undefined) {
|
|
199
|
+
recordEvent(type);
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
sessionIdValue = id;
|
|
203
|
+
out.push(JSON.stringify({
|
|
204
|
+
type: "session",
|
|
205
|
+
id,
|
|
206
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
207
|
+
}));
|
|
208
|
+
out.push(JSON.stringify({ type: "agent_start" }));
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
case "turn.started":
|
|
212
|
+
return [];
|
|
213
|
+
case "turn.completed": {
|
|
214
|
+
sawTurnCompleted = true;
|
|
215
|
+
const usage = objectOrEmpty(event.usage);
|
|
216
|
+
turnUsage =
|
|
217
|
+
usage.input_tokens !== undefined || usage.output_tokens !== undefined
|
|
218
|
+
? {
|
|
219
|
+
input: nonNegativeNumber(usage.input_tokens, 0),
|
|
220
|
+
cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
|
|
221
|
+
cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
|
|
222
|
+
output: nonNegativeNumber(usage.output_tokens, 0),
|
|
223
|
+
}
|
|
224
|
+
: undefined;
|
|
225
|
+
return abandonOpenCalls();
|
|
226
|
+
}
|
|
227
|
+
case "turn.failed": {
|
|
228
|
+
sawTurnFailed = true;
|
|
229
|
+
// The error message is itself a JSON string (finding 11); fall back to the held
|
|
230
|
+
// top-level error text when it is not.
|
|
231
|
+
const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
|
|
232
|
+
? event.error.message
|
|
233
|
+
: undefined;
|
|
234
|
+
const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
|
|
235
|
+
const cls = inner ? classifyTurnFailed(inner) : {};
|
|
236
|
+
const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
|
|
237
|
+
const closes = abandonOpenCalls();
|
|
238
|
+
const message = { role: "assistant", content: [], stopReason: "error" };
|
|
239
|
+
if (errorMessage !== undefined)
|
|
240
|
+
message.errorMessage = errorMessage;
|
|
241
|
+
const end = {
|
|
242
|
+
type: "message_end",
|
|
243
|
+
message,
|
|
244
|
+
...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
|
|
245
|
+
...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
|
|
246
|
+
};
|
|
247
|
+
if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
|
|
248
|
+
end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
|
|
249
|
+
return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
|
|
250
|
+
}
|
|
251
|
+
case "error": {
|
|
252
|
+
const message = typeof event.message === "string" ? event.message : undefined;
|
|
253
|
+
const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
|
|
254
|
+
if (reconnect) {
|
|
255
|
+
return [
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
type: "auto_retry_start",
|
|
258
|
+
attempt: Number(reconnect[1]),
|
|
259
|
+
maxAttempts: Number(reconnect[2]),
|
|
260
|
+
errorMessage: message,
|
|
261
|
+
}),
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
// Other top-level `error` lines are not fatal (design §5): held as the fallback text
|
|
265
|
+
// for a following `turn.failed`.
|
|
266
|
+
if (message !== undefined)
|
|
267
|
+
pendingFailureText = message;
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
case "item.started":
|
|
271
|
+
case "item.completed": {
|
|
272
|
+
const item = objectOrEmpty(event.item);
|
|
273
|
+
const itemType = typeof item.type === "string" ? item.type : "unknown";
|
|
274
|
+
const started = type === "item.started";
|
|
275
|
+
if (itemType === "command_execution") {
|
|
276
|
+
const command = typeof item.command === "string" ? item.command : "";
|
|
277
|
+
if (started) {
|
|
278
|
+
const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
|
|
279
|
+
openCalls.set(id, "command_execution");
|
|
280
|
+
const inner = innerCommand(command);
|
|
281
|
+
out.push(JSON.stringify({
|
|
282
|
+
type: "message_end",
|
|
283
|
+
message: {
|
|
284
|
+
role: "assistant",
|
|
285
|
+
content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
|
|
286
|
+
},
|
|
287
|
+
}));
|
|
288
|
+
out.push(JSON.stringify({
|
|
289
|
+
type: "tool_execution_start",
|
|
290
|
+
toolCallId: id,
|
|
291
|
+
toolName: "bash",
|
|
292
|
+
args: { command: inner },
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
297
|
+
openCalls.delete(id);
|
|
298
|
+
const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
299
|
+
const exitCode = nonNegativeNumber(item.exit_code, undefined);
|
|
300
|
+
out.push(JSON.stringify({
|
|
301
|
+
type: "tool_execution_end",
|
|
302
|
+
toolCallId: id,
|
|
303
|
+
isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
|
|
304
|
+
result: boundText(aggregated, bashLimit),
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
if (itemType === "file_change") {
|
|
310
|
+
const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
|
|
311
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
312
|
+
if (!started) {
|
|
313
|
+
const isError = typeof item.status === "string" && item.status !== "completed";
|
|
314
|
+
for (let i = 0; i < changes.length; i++) {
|
|
315
|
+
const callId = `${id}-${i}`;
|
|
316
|
+
openCalls.delete(callId);
|
|
317
|
+
out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
const content = changes.map((c, i) => ({
|
|
322
|
+
type: "toolCall",
|
|
323
|
+
id: `${id}-${i}`,
|
|
324
|
+
name: "edit",
|
|
325
|
+
arguments: {
|
|
326
|
+
path: typeof c.path === "string" ? c.path : undefined,
|
|
327
|
+
kind: typeof c.kind === "string" ? c.kind : undefined,
|
|
328
|
+
},
|
|
329
|
+
}));
|
|
330
|
+
if (content.length === 0)
|
|
331
|
+
return out; // no changes to mirror, nothing else to emit
|
|
332
|
+
out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
|
|
333
|
+
for (let i = 0; i < content.length; i++) {
|
|
334
|
+
const callId = `${id}-${i}`;
|
|
335
|
+
openCalls.set(callId, "file_change");
|
|
336
|
+
out.push(JSON.stringify({
|
|
337
|
+
type: "tool_execution_start",
|
|
338
|
+
toolCallId: callId,
|
|
339
|
+
toolName: "edit",
|
|
340
|
+
args: content[i].arguments,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
if (itemType === "error") {
|
|
346
|
+
// An error item is a non-fatal notice (finding 11); warned once per distinct message.
|
|
347
|
+
const message = typeof item.message === "string" ? item.message : undefined;
|
|
348
|
+
if (message !== undefined)
|
|
349
|
+
recordErrorItem(message);
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
if (itemType === "agent_message") {
|
|
353
|
+
// The model's reply: one canonical `message_end` with one text block.
|
|
354
|
+
const text = typeof item.text === "string" ? item.text : "";
|
|
355
|
+
return [
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
type: "message_end",
|
|
358
|
+
message: { role: "assistant", content: [{ type: "text", text }] },
|
|
359
|
+
}),
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
if (DROPPED_ITEM_TYPES.has(itemType))
|
|
363
|
+
return [];
|
|
364
|
+
// mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
|
|
365
|
+
// once-per-type warning only (design §5).
|
|
366
|
+
recordItem(item);
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
recordEvent(type);
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
return {
|
|
375
|
+
translate,
|
|
376
|
+
sessionId() {
|
|
377
|
+
return sessionIdValue;
|
|
378
|
+
},
|
|
379
|
+
turnCompletedUsage() {
|
|
380
|
+
return turnUsage;
|
|
381
|
+
},
|
|
382
|
+
flagUsagePartial() {
|
|
383
|
+
warnings.add("codex_usage_partial");
|
|
384
|
+
},
|
|
385
|
+
flagModelSwitched(requested, ran) {
|
|
386
|
+
warnings.add(`codex_model_switched requested=${requested} ran=${ran}`);
|
|
387
|
+
},
|
|
388
|
+
finalize() {
|
|
389
|
+
// The stream ended with neither turn.completed nor turn.failed (a SIGTERM the harness
|
|
390
|
+
// did not send, finding 12): report the turn never finished.
|
|
391
|
+
if (sawTurnCompleted || sawTurnFailed)
|
|
392
|
+
return [];
|
|
393
|
+
return [
|
|
394
|
+
JSON.stringify({
|
|
395
|
+
type: "message_end",
|
|
396
|
+
message: { role: "assistant", content: [], stopReason: "error", errorMessage: "codex exited without finishing its turn" },
|
|
397
|
+
}),
|
|
398
|
+
];
|
|
399
|
+
},
|
|
400
|
+
warnings() {
|
|
401
|
+
return [...warnings];
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|