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,279 @@
|
|
|
1
|
+
/** The codex adapter for the agent seam (plans/codex-backend.md §4, §5, §6): it shapes one
|
|
2
|
+
* run's argv/stdin/env, translates codex's `exec --json` stream, reads the authoritative
|
|
3
|
+
* rollout after the process exits, and classifies the exit's stderr — everything
|
|
4
|
+
* backend-specific. src/agent/run-agent.ts owns the spawn and records this run in the session
|
|
5
|
+
* ledger; codex mints its own session id (a thread id, or the resumed record's id) in the
|
|
6
|
+
* stream's `thread.started`, which the runner's stream-mint pass appends to the ledger and
|
|
7
|
+
* `invocation` returns so a later `exec resume` can reach it.
|
|
8
|
+
*
|
|
9
|
+
* Sessions resume by id, out of the team's `CODEX_HOME`. A continue takes the ledger's newest
|
|
10
|
+
* record of this run's kind and resumes only when it is codex's, has this cwd, carries a UUID
|
|
11
|
+
* id, and `findRollout` finds its rollout under the run's home — anything else mints a fresh
|
|
12
|
+
* thread and the existing `handoff` path takes over (design §4). Together with the `resumable`
|
|
13
|
+
* hook, that is every resume point's one rule.
|
|
14
|
+
*
|
|
15
|
+
* Registration lives in src/agent/run-agent.ts beside the pi, claude and opencode adapters; a
|
|
16
|
+
* test selects codex by setting `config.agent = "codex"` on a run. A run carrying a note gets
|
|
17
|
+
* the notes path in its environment (the `tumwater-notes` command on PATH reads it), as the
|
|
18
|
+
* pi and claude adapters do. */
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import { loadConfigSafe } from "../../config/config.js";
|
|
21
|
+
import { resolvedBackendConfig } from "../../config/config-views.js";
|
|
22
|
+
import { backendRunEnv } from "../../process/run-marker.js";
|
|
23
|
+
import { resolveBinForBackend } from "../../pi/pi-bin.js";
|
|
24
|
+
import { teamRootOf, nearestTumwaterRoot } from "../../paths.js";
|
|
25
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../../events/events.js";
|
|
26
|
+
import { errorMessage } from "../../text/text.js";
|
|
27
|
+
import { codexHome, readCodexCatalog } from "./codex-catalog.js";
|
|
28
|
+
import { resumableSession } from "../session-ledger.js";
|
|
29
|
+
import { codexArgs, codexEffort } from "./codex-args.js";
|
|
30
|
+
import { codexEnv } from "./codex-env.js";
|
|
31
|
+
import { codexHookTrust } from "./codex-hook-trust.js";
|
|
32
|
+
import { codexHookArg, codexHookTrustArg, codexWriteGuardDefinition } from "./codex-write-guard.js";
|
|
33
|
+
import { findRollout, rolloutLines } from "./codex-rollout.js";
|
|
34
|
+
import { codexTranslator } from "./codex-stream.js";
|
|
35
|
+
/** A codex thread id (a UUID format, e.g. `01a15c9d-6762-7931-814e-a3e6e64c1b51`). The
|
|
36
|
+
* stream mints it in `thread.started`; the pre-spawn ledger record carries the session name
|
|
37
|
+
* instead, so this regex is what tells a resume that a record holds a real, stream-minted id
|
|
38
|
+
* worth resuming (design §4). */
|
|
39
|
+
const THREAD_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
40
|
+
/** The oldest codex CLI the team targets, the sibling of `OPENCODE_MIN_VERSION`
|
|
41
|
+
* (src/agent/opencode/opencode-backend.ts) and `CLAUDE_MIN_VERSION`
|
|
42
|
+
* (src/agent/claude/claude-backend.ts): doctor's `codex version` row fails an installed CLI
|
|
43
|
+
* below this floor (plans/codex-backend.md "Hand steps", H1). */
|
|
44
|
+
export const CODEX_MIN_VERSION = "0.163.0";
|
|
45
|
+
/** The wall-clock bound `prepare` gives each trust lookup, mirroring lookupCodexHookTrust's
|
|
46
|
+
* own 15 s default (plans/codex-write-guard.md "Trust (parts 4–5)"). Settable so a test can
|
|
47
|
+
* shorten the bound (to 500 ms) without waiting out the real 15 s; a hand run keeps the
|
|
48
|
+
* default. */
|
|
49
|
+
let codexTrustLookupTimeoutMs = 15_000;
|
|
50
|
+
/** The test seam for the trust lookup bound: a test sets it to 500 ms and returns it to the
|
|
51
|
+
* 15 s default in a finally, so a hanging app-server case stays fast without a config knob. */
|
|
52
|
+
export function setCodexTrustLookupTimeoutMs(ms) {
|
|
53
|
+
codexTrustLookupTimeoutMs = ms;
|
|
54
|
+
}
|
|
55
|
+
/** The write-guard trust `prepare` has resolved, keyed by home and hook definition, so one
|
|
56
|
+
* process looks each up at most once and a run's invocation never spawns `app-server` again. */
|
|
57
|
+
const writeGuardTrust = new Map();
|
|
58
|
+
/** One in-flight resolution per (home, definition) key, so concurrent runs share a single
|
|
59
|
+
* `app-server` instead of starting one each (plans/codex-write-guard.md "Trust (parts 4–5)"). */
|
|
60
|
+
const writeGuardInFlight = new Map();
|
|
61
|
+
/** The (home, definition) keys whose `off` warning has already been logged, so a failed lookup
|
|
62
|
+
* warns once per process instead of once per run. */
|
|
63
|
+
const writeGuardWarned = new Set();
|
|
64
|
+
/** Warn once per process per (home, definition) that the write guard is off. The warning rides
|
|
65
|
+
* the best-effort event feed on the run's root, skipped when there is no root (a hand run's
|
|
66
|
+
* prepared look-up must not mint a `.tumwater`). A `trust` result logs nothing. */
|
|
67
|
+
function warnGuardOffOnce(key, root, state) {
|
|
68
|
+
if (state.kind !== "off")
|
|
69
|
+
return;
|
|
70
|
+
if (writeGuardWarned.has(key))
|
|
71
|
+
return;
|
|
72
|
+
writeGuardWarned.add(key);
|
|
73
|
+
if (root !== undefined) {
|
|
74
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `codex write guard off: ${state.reason}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** The (home, definition) cache key shared by the single-flight and the resolved-trust map. */
|
|
78
|
+
function writeGuardKey(home, definition) {
|
|
79
|
+
return `${home ?? ""}|${JSON.stringify(definition)}`;
|
|
80
|
+
}
|
|
81
|
+
/** The session this run uses: for a continue, the ledger's newest record of this kind when it
|
|
82
|
+
* is codex's, has this cwd, carries a UUID thread id, and `findRollout` finds its rollout
|
|
83
|
+
* under the run's home; otherwise the run is fresh (no pre-minted id — the stream names it).
|
|
84
|
+
* Returns the id and whether this is a resume, so `invocation` can name it and build `exec
|
|
85
|
+
* resume`. */
|
|
86
|
+
function resolveCodexSession(run, home) {
|
|
87
|
+
if (run.continueSession && home !== undefined) {
|
|
88
|
+
const record = resumableSession(run.sessionDir, {
|
|
89
|
+
kind: run.kind,
|
|
90
|
+
agent: "codex",
|
|
91
|
+
cwd: run.cwd,
|
|
92
|
+
});
|
|
93
|
+
if (record !== null && THREAD_ID_RE.test(record.id) && findRollout(home, record.id) !== null) {
|
|
94
|
+
return { sessionId: record.id, resumed: true };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { resumed: false };
|
|
98
|
+
}
|
|
99
|
+
/** Classify codex's stderr: the seam's stderr half for a CLI that died before emitting a
|
|
100
|
+
* `turn.failed` (the stream's own failures are classified by the translator). A run the
|
|
101
|
+
* harness killed has its own cause and is never classified here (design finding 12). The
|
|
102
|
+
* codex CLI surfaces login/model/usage failures as stream JSON, so a bare nonzero exit with
|
|
103
|
+
* no stream carries no signal this part has a verdict for — it stays unclassified and the
|
|
104
|
+
* parser's own event-derived flags stand. */
|
|
105
|
+
function classifyCodexExit(info) {
|
|
106
|
+
if (info.killed)
|
|
107
|
+
return {};
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
/** The agent-backend seam's codex adapter. */
|
|
111
|
+
export const codexBackend = {
|
|
112
|
+
id: "codex",
|
|
113
|
+
label: "Codex",
|
|
114
|
+
defaultBin: "codex",
|
|
115
|
+
binEnv: "TUMWATER_CODEX_BIN",
|
|
116
|
+
resolveBin(config) {
|
|
117
|
+
return resolveBinForBackend(config, this);
|
|
118
|
+
},
|
|
119
|
+
invocation(run) {
|
|
120
|
+
// The notes path rides beside the run marker, as the pi and claude adapters' envs do
|
|
121
|
+
// (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes`
|
|
122
|
+
// command on PATH, so codex configures no MCP server for notes.
|
|
123
|
+
const base = backendRunEnv(run);
|
|
124
|
+
// A team home of its own (`CODEX_HOME`) keeps team sessions and trust entries out of the
|
|
125
|
+
// operator's `~/.codex` (design §4, finding 10); unset leaves the operator's home.
|
|
126
|
+
const home = codexHome(resolvedBackendConfig(run.config, "codex"));
|
|
127
|
+
const env = codexEnv(base, { home });
|
|
128
|
+
const { sessionId, resumed } = resolveCodexSession(run, home);
|
|
129
|
+
const thinking = run.config.thinking;
|
|
130
|
+
// The effort (parts 5/9 and 8/9): ride the thinking level through the decoder, then gate
|
|
131
|
+
// it on the model's cataloged effort levels — a level the model does not list is never
|
|
132
|
+
// sent (codexEffort's `levels` half). No home (no catalog) leaves the value unclamped, as
|
|
133
|
+
// before part 8/9.
|
|
134
|
+
const catalog = home === undefined ? new Map() : readCodexCatalog(home);
|
|
135
|
+
const effort = thinking === undefined ? undefined : codexEffort(thinking, catalog.get(run.config.model ?? "")?.levels);
|
|
136
|
+
// The write guard's trust (part 6/9): `prepare` resolved it under this run's env and
|
|
137
|
+
// home and stored it keyed by (home, definition), so `invocation` reads it synchronously.
|
|
138
|
+
// With trust it rides the hook and its trust entry as two `-c` values (after the
|
|
139
|
+
// harness's own keys, before `-m`); with `off` or no state it passes neither, since an
|
|
140
|
+
// untrusted hook would be skipped silently anyway. tumwater never passes
|
|
141
|
+
// `--dangerously-bypass-hook-trust` (findings 1 and 4).
|
|
142
|
+
const definition = codexWriteGuardDefinition();
|
|
143
|
+
const trust = writeGuardTrust.get(writeGuardKey(home, definition));
|
|
144
|
+
const guardArgs = trust?.kind === "trust"
|
|
145
|
+
? ["-c", codexHookArg(definition), "-c", codexHookTrustArg(trust.trust)]
|
|
146
|
+
: undefined;
|
|
147
|
+
const { args, stdin } = codexArgs({
|
|
148
|
+
model: run.config.model ?? "",
|
|
149
|
+
...(effort !== undefined ? { effort } : {}),
|
|
150
|
+
...(resumed && sessionId !== undefined ? { resumeId: sessionId } : {}),
|
|
151
|
+
...(guardArgs !== undefined ? { guardArgs } : {}),
|
|
152
|
+
args: resolvedBackendConfig(run.config, "codex").args,
|
|
153
|
+
prompt: run.prompt,
|
|
154
|
+
});
|
|
155
|
+
// A resumed run returns its id so the runner's pre-spawn ledger record carries it; a fresh
|
|
156
|
+
// run has none until the stream names it (the runner's stream-mint pass appends it).
|
|
157
|
+
return { args, stdin, env, ...(resumed && sessionId !== undefined ? { sessionId } : {}) };
|
|
158
|
+
},
|
|
159
|
+
async prepare(run) {
|
|
160
|
+
// Resolve the write guard's trust under the same env and home invocation will use (the
|
|
161
|
+
// run's CODEX_HOME, plans/codex-write-guard.md "Trust (parts 4–5)"), so the trust an
|
|
162
|
+
// invocation later reads was looked up under the run's own config. It never throws a run
|
|
163
|
+
// away: a failed lookup records `off` (and warns once) and the run continues without the
|
|
164
|
+
// guard's trust.
|
|
165
|
+
const base = backendRunEnv(run);
|
|
166
|
+
const home = codexHome(resolvedBackendConfig(run.config, "codex"));
|
|
167
|
+
const env = codexEnv(base, { home });
|
|
168
|
+
const root = nearestTumwaterRoot(run.sessionDir);
|
|
169
|
+
const definition = codexWriteGuardDefinition();
|
|
170
|
+
const key = writeGuardKey(home, definition);
|
|
171
|
+
// Single-flight per (home, definition): a run whose key is being resolved (or already
|
|
172
|
+
// resolved) shares the one in-flight lookup, so concurrent runs start one `app-server`.
|
|
173
|
+
const settled = writeGuardTrust.get(key);
|
|
174
|
+
if (settled !== undefined) {
|
|
175
|
+
warnGuardOffOnce(key, root, settled);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const inFlight = writeGuardInFlight.get(key);
|
|
179
|
+
if (inFlight !== undefined) {
|
|
180
|
+
await inFlight;
|
|
181
|
+
const after = writeGuardTrust.get(key);
|
|
182
|
+
if (after !== undefined)
|
|
183
|
+
warnGuardOffOnce(key, root, after);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const promise = (async () => {
|
|
187
|
+
try {
|
|
188
|
+
const trust = await codexHookTrust(root, {
|
|
189
|
+
bin: run.agentBin,
|
|
190
|
+
env,
|
|
191
|
+
cwd: run.cwd,
|
|
192
|
+
definition,
|
|
193
|
+
timeoutMs: codexTrustLookupTimeoutMs,
|
|
194
|
+
});
|
|
195
|
+
writeGuardTrust.set(key, { kind: "trust", trust });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
writeGuardTrust.set(key, { kind: "off", reason: errorMessage(error) });
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
writeGuardInFlight.set(key, promise);
|
|
202
|
+
await promise;
|
|
203
|
+
writeGuardInFlight.delete(key);
|
|
204
|
+
const state = writeGuardTrust.get(key);
|
|
205
|
+
if (state !== undefined)
|
|
206
|
+
warnGuardOffOnce(key, root, state);
|
|
207
|
+
},
|
|
208
|
+
translator() {
|
|
209
|
+
return codexTranslator();
|
|
210
|
+
},
|
|
211
|
+
imageInput(model) {
|
|
212
|
+
// Answer from the model catalog (part 8/9): a model reads images when its catalog entry's
|
|
213
|
+
// `input_modalities` say so; a model missing from the catalog — or a run with no home to
|
|
214
|
+
// read — answers false, the safe direction, so a blind image read is never silently
|
|
215
|
+
// assumed readable. The home is the team's `backends.codex.configDir`, found by walking
|
|
216
|
+
// up from the process's working directory to its `.tumwater` root and loading the config
|
|
217
|
+
// there, the same root the runs' home and account live under (like opencode's cache).
|
|
218
|
+
if (model === undefined)
|
|
219
|
+
return false;
|
|
220
|
+
const root = teamRootOf(process.cwd());
|
|
221
|
+
const { config } = loadConfigSafe(root);
|
|
222
|
+
const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
|
|
223
|
+
if (home === undefined)
|
|
224
|
+
return false;
|
|
225
|
+
return readCodexCatalog(home).get(model)?.images === true;
|
|
226
|
+
},
|
|
227
|
+
resumable(dir, opts) {
|
|
228
|
+
const record = resumableSession(dir, { kind: opts.kind, agent: "codex", cwd: opts.cwd });
|
|
229
|
+
if (record === null || !THREAD_ID_RE.test(record.id))
|
|
230
|
+
return false;
|
|
231
|
+
const root = teamRootOf(dir);
|
|
232
|
+
const { config } = loadConfigSafe(root);
|
|
233
|
+
const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
|
|
234
|
+
if (home === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
return findRollout(home, record.id) !== null;
|
|
237
|
+
},
|
|
238
|
+
afterRun(run, invocation, translator) {
|
|
239
|
+
const ct = translator;
|
|
240
|
+
const home = codexHome(resolvedBackendConfig(run.config, "codex"));
|
|
241
|
+
const id = invocation.sessionId ?? ct.sessionId?.();
|
|
242
|
+
const fresh = invocation.sessionId === undefined;
|
|
243
|
+
// The rollout is authoritative, so its loss is worth one warning: a fresh run keeps the
|
|
244
|
+
// stream's own usage as a fallback, a resumed run records none (design §5, finding 4).
|
|
245
|
+
const fallback = () => {
|
|
246
|
+
ct.flagUsagePartial();
|
|
247
|
+
if (!fresh)
|
|
248
|
+
return [];
|
|
249
|
+
const usage = ct.turnCompletedUsage();
|
|
250
|
+
return usage !== undefined ? [JSON.stringify({ type: "run_usage", ...usage })] : [];
|
|
251
|
+
};
|
|
252
|
+
if (home === undefined || id === undefined)
|
|
253
|
+
return fallback();
|
|
254
|
+
const rollout = findRollout(home, id);
|
|
255
|
+
if (rollout === null)
|
|
256
|
+
return fallback();
|
|
257
|
+
let text;
|
|
258
|
+
try {
|
|
259
|
+
text = fs.readFileSync(rollout, "utf8");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return fallback();
|
|
263
|
+
}
|
|
264
|
+
// The stream never exposes the turn id — only the thread id that names the rollout file
|
|
265
|
+
// (real captures' `task_started.turn_id` is a different UUID), so the reader selects the
|
|
266
|
+
// last block, this run's, from the file the thread id located.
|
|
267
|
+
const { lines, switchedTo } = rolloutLines(text, {
|
|
268
|
+
requestedModel: run.config.model ?? "",
|
|
269
|
+
});
|
|
270
|
+
if (lines.length === 0)
|
|
271
|
+
return fallback();
|
|
272
|
+
// Codex can switch models on a usage limit by itself (finding 24): warn once and record
|
|
273
|
+
// the model that actually ran in the rollout's reading.
|
|
274
|
+
if (switchedTo !== undefined)
|
|
275
|
+
ct.flagModelSwitched(run.config.model ?? "", switchedTo);
|
|
276
|
+
return lines;
|
|
277
|
+
},
|
|
278
|
+
classifyExit: classifyCodexExit,
|
|
279
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** The Codex model catalog (plans/codex-backend.md design finding 14 — "Spend, free-ness,
|
|
2
|
+
* window, images" part 8/9): tumwater reads a Codex model's effort levels, default effort,
|
|
3
|
+
* context window and image input from `$CODEX_HOME/models_cache.json`, the file the CLI itself
|
|
4
|
+
* refreshes and `codex debug models` prints. Before part 8/9 the adapter assumed every Codex
|
|
5
|
+
* model had every effort and read every image; the catalog refines both, and seats the context
|
|
6
|
+
* window `seamContextWindow` reports for a codex seam. There are no prices in the catalog — the
|
|
7
|
+
* whole file knows nothing of cost — so free-ness never reads here; the codex account
|
|
8
|
+
* (codex-account.ts) owns billing.
|
|
9
|
+
*
|
|
10
|
+
* A missing, unreadable or malformed file reads as an empty catalog, the same "no data" policy
|
|
11
|
+
* pi's models.json reader uses: the adapter then answers blind (no effort gating, no images,
|
|
12
|
+
* no window), which is the safe direction for the window and image checks. The parse itself is
|
|
13
|
+
* pure and build-agnostic — no registry, config or subprocess — so the adapter, pi's
|
|
14
|
+
* seamContextWindow and the doctor can read one answer without dragging a runtime in. */
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { cachedByStat } from "../../files/stat-cache.js";
|
|
18
|
+
import { finiteNumber, isJsonObject } from "../../files/json-object.js";
|
|
19
|
+
import { readJsonFile } from "../../files/json-files.js";
|
|
20
|
+
import { expandHomeTilde } from "../../files/files.js";
|
|
21
|
+
/** The team's Codex home for a run: `backends.codex.configDir`, a leading `~` expanded, else
|
|
22
|
+
* the CLI's default `~/.codex` — a run with no configured home keeps the operator's
|
|
23
|
+
* `CODEX_HOME`, which is exactly Codex's own default home, so the catalog and the sessions
|
|
24
|
+
* live there too and a known home always exists. Pure. Shared by the adapter, pi's
|
|
25
|
+
* seamContextWindow and the codex-account seed, so all three read one home. */
|
|
26
|
+
export function codexHome(config) {
|
|
27
|
+
const dir = config.configDir;
|
|
28
|
+
if (dir === undefined)
|
|
29
|
+
return path.join(os.homedir(), ".codex");
|
|
30
|
+
return expandHomeTilde(dir, os.homedir());
|
|
31
|
+
}
|
|
32
|
+
/** Where a home's model catalog lives: `$CODEX_HOME/models_cache.json`. */
|
|
33
|
+
export function codexModelsPath(home) {
|
|
34
|
+
return path.join(home, "models_cache.json");
|
|
35
|
+
}
|
|
36
|
+
/** Per-catalog cache of the parsed entries, keyed by the models_cache path: the status
|
|
37
|
+
* surfaces read the window every poll while the file changes only when the CLI refreshes it —
|
|
38
|
+
* and it grows with the model catalog — so an unchanged file costs one stat per poll instead
|
|
39
|
+
* of a re-read plus a JSON.parse of the whole file. Any write invalidates via
|
|
40
|
+
* dev/ino/mtime/size (stat-cache.cachedByStat, the same freshness check pi's providers use). */
|
|
41
|
+
const catalogCache = new Map();
|
|
42
|
+
/** The parsed model catalog from `$CODEX_HOME/models_cache.json`, keyed by model slug, or an
|
|
43
|
+
* empty map when the file is missing, unreadable, malformed or not shaped like what the CLI
|
|
44
|
+
* writes. A model's entry is kept only when it names a usable slug and context_window; a
|
|
45
|
+
* missing `effective_context_window_percent` reads as no window (the model is dropped), since
|
|
46
|
+
* the window is the whole point of the entry. The returned map is a copy — callers may treat
|
|
47
|
+
* their result as their own. */
|
|
48
|
+
export function readCodexCatalog(home) {
|
|
49
|
+
const modelsPath = codexModelsPath(home);
|
|
50
|
+
return (cachedByStat(catalogCache, modelsPath, modelsPath, () => {
|
|
51
|
+
const out = new Map();
|
|
52
|
+
// readJsonFile is the shared "missing or torn reads as no data" policy for a JSON
|
|
53
|
+
// object file; the nested `models` array gets the same isJsonObject/Array guards.
|
|
54
|
+
const doc = readJsonFile(modelsPath);
|
|
55
|
+
if (doc === null)
|
|
56
|
+
return out;
|
|
57
|
+
const models = doc.models;
|
|
58
|
+
if (!Array.isArray(models))
|
|
59
|
+
return out;
|
|
60
|
+
for (const model of models) {
|
|
61
|
+
const entry = parseModelEntry(model);
|
|
62
|
+
if (entry !== undefined)
|
|
63
|
+
out.set(entry.slug, entry.entry);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}, (models) => new Map([...models].map(([slug, entry]) => [slug, { ...entry, levels: [...entry.levels] }]))) ?? new Map());
|
|
67
|
+
}
|
|
68
|
+
/** One catalog model entry as the map needs it: the slug to key by plus the parsed entry. The
|
|
69
|
+
* entry is kept only when the slug and context window are usable; any other missing field
|
|
70
|
+
* (levels, modalities, default level) just reads as absent on the entry. */
|
|
71
|
+
function parseModelEntry(model) {
|
|
72
|
+
if (!isJsonObject(model) || typeof model.slug !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
const context = finiteNumber(model.context_window, Number.NaN);
|
|
75
|
+
const percent = finiteNumber(model.effective_context_window_percent, Number.NaN);
|
|
76
|
+
if (!Number.isFinite(context) || !Number.isFinite(percent))
|
|
77
|
+
return undefined;
|
|
78
|
+
const levels = [];
|
|
79
|
+
if (Array.isArray(model.supported_reasoning_levels)) {
|
|
80
|
+
for (const level of model.supported_reasoning_levels) {
|
|
81
|
+
if (isJsonObject(level) && typeof level.effort === "string")
|
|
82
|
+
levels.push(level.effort);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const modalities = model.input_modalities;
|
|
86
|
+
return {
|
|
87
|
+
slug: model.slug,
|
|
88
|
+
entry: {
|
|
89
|
+
levels,
|
|
90
|
+
...(typeof model.default_reasoning_level === "string"
|
|
91
|
+
? { defaultLevel: model.default_reasoning_level }
|
|
92
|
+
: {}),
|
|
93
|
+
contextWindow: Math.round((context * percent) / 100),
|
|
94
|
+
images: Array.isArray(modalities) && modalities.includes("image"),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Build the codex process environment from `base`. Never mutates `base`. */
|
|
2
|
+
export function codexEnv(base, opts = {}) {
|
|
3
|
+
const env = {};
|
|
4
|
+
for (const [key, value] of Object.entries(base)) {
|
|
5
|
+
if (key === "CLAUDECODE")
|
|
6
|
+
continue;
|
|
7
|
+
if (key.startsWith("CODEX_"))
|
|
8
|
+
continue;
|
|
9
|
+
if (key.startsWith("CLAUDE_CODE_"))
|
|
10
|
+
continue;
|
|
11
|
+
if (key === "OPENAI_API_KEY" || key === "CODEX_API_KEY")
|
|
12
|
+
continue;
|
|
13
|
+
env[key] = value;
|
|
14
|
+
}
|
|
15
|
+
if (opts.home)
|
|
16
|
+
env.CODEX_HOME = opts.home;
|
|
17
|
+
return env;
|
|
18
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/** How tumwater learns the write guard's trust from codex itself (plans/codex-write-guard.md
|
|
2
|
+
* "Trust (parts 4–5)", findings 2–3): codex runs a session-flag PreToolUse hook only when its
|
|
3
|
+
* `trusted_hash` matches the hash codex computed for the hook's definition, and that hash is
|
|
4
|
+
* opaque — no simple digest of any command or JSON spelling matched (finding 3). So this module
|
|
5
|
+
* asks codex's own `hooks/list` RPC for the hook's `key` and `currentHash`, and caches the
|
|
6
|
+
* answer per build and codex version so a run's argv never spawns `app-server` again. The
|
|
7
|
+
* answer is never hardcoded: the key (`/<session-flags>/config.toml:pre_tool_use:0:0` on
|
|
8
|
+
* 0.163.0) and the hash both come from this lookup.
|
|
9
|
+
*
|
|
10
|
+
* The lookup spawns `app-server` in its own process group under the run's env and a bounded
|
|
11
|
+
* deadline, kills the group whether it succeeds, times out, or fails, and makes no model
|
|
12
|
+
* request and needs no login (an empty `CODEX_HOME` is enough — finding 3). Part 5/9 wires it
|
|
13
|
+
* into a run before the codex adapter's synchronous invocation. */
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { codexHookArg } from "./codex-write-guard.js";
|
|
16
|
+
import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
|
|
17
|
+
import { releaseChildHandles, signalTree } from "../../process/process.js";
|
|
18
|
+
import { codexHookTrustPath } from "../../paths.js";
|
|
19
|
+
import { PACKAGE_JSON, packageVersion } from "../../version.js";
|
|
20
|
+
/** The JSON-RPC request/notification sequence one lookup sends over `app-server`'s stdio: the
|
|
21
|
+
* `initialize` handshake (carrying tumwater's own version), the `initialized` notification,
|
|
22
|
+
* then `hooks/list` for the one cwd. The reply to `id: 2` carries the hooks. */
|
|
23
|
+
function hookListMessages(cwd) {
|
|
24
|
+
const version = packageVersion(PACKAGE_JSON).version;
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
jsonrpc: "2.0",
|
|
28
|
+
id: 1,
|
|
29
|
+
method: "initialize",
|
|
30
|
+
params: { clientInfo: { name: "tumwater", version: version ?? "unknown" } },
|
|
31
|
+
},
|
|
32
|
+
{ jsonrpc: "2.0", method: "initialized" },
|
|
33
|
+
{ jsonrpc: "2.0", id: 2, method: "hooks/list", params: { cwds: [cwd] } },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
/** The first hook in `hooks/list`'s `result.data[*].hooks` with `source: "sessionFlags"`,
|
|
37
|
+
* `eventName: "preToolUse"` and `command` exactly the guard definition's (finding 3), or null
|
|
38
|
+
* when none matches. The key and hash are returned as codex reports them. */
|
|
39
|
+
function findTrust(result, definition) {
|
|
40
|
+
if (typeof result !== "object" || result === null)
|
|
41
|
+
return null;
|
|
42
|
+
const data = result.data;
|
|
43
|
+
if (!Array.isArray(data))
|
|
44
|
+
return null;
|
|
45
|
+
for (const group of data) {
|
|
46
|
+
if (typeof group !== "object" || group === null)
|
|
47
|
+
continue;
|
|
48
|
+
const hooks = group.hooks;
|
|
49
|
+
if (!Array.isArray(hooks))
|
|
50
|
+
continue;
|
|
51
|
+
for (const hook of hooks) {
|
|
52
|
+
if (typeof hook !== "object" || hook === null)
|
|
53
|
+
continue;
|
|
54
|
+
const h = hook;
|
|
55
|
+
if (h.source !== "sessionFlags" || h.eventName !== "preToolUse")
|
|
56
|
+
continue;
|
|
57
|
+
if (h.command !== definition.command)
|
|
58
|
+
continue;
|
|
59
|
+
if (typeof h.key === "string" && typeof h.currentHash === "string") {
|
|
60
|
+
return { key: h.key, hash: h.currentHash };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** A short name for a JSON-RPC `error` value, for the rejection message. */
|
|
67
|
+
function describeError(error) {
|
|
68
|
+
if (typeof error === "string")
|
|
69
|
+
return error;
|
|
70
|
+
if (typeof error === "object" && error !== null) {
|
|
71
|
+
const message = error.message;
|
|
72
|
+
if (typeof message === "string")
|
|
73
|
+
return message;
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(error);
|
|
76
|
+
}
|
|
77
|
+
/** Ask codex for the guard hook's trust. Spawns `<bin> -c <codexHookArg(definition)>
|
|
78
|
+
* app-server` in `cwd` under `env`, in its own process group, sends the JSON-RPC sequence
|
|
79
|
+
* above over stdio, and resolves with the trust from the first matching `hooks/list` hook.
|
|
80
|
+
*
|
|
81
|
+
* It rejects on a timeout (`timeoutMs`), on the child exiting before the reply, on a JSON-RPC
|
|
82
|
+
* `error`, and on codex listing no session-flag PreToolUse hook with this command. The child's
|
|
83
|
+
* process group is SIGKILLed whether the lookup settles by success, failure, or timeout, so no
|
|
84
|
+
* `app-server` outlives the call. */
|
|
85
|
+
export function lookupCodexHookTrust(opts) {
|
|
86
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const child = spawn(opts.bin, ["-c", codexHookArg(opts.definition), "app-server"], {
|
|
89
|
+
cwd: opts.cwd,
|
|
90
|
+
env: opts.env,
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
93
|
+
});
|
|
94
|
+
let settled = false;
|
|
95
|
+
let buffer = "";
|
|
96
|
+
const finish = (err, trust) => {
|
|
97
|
+
if (settled)
|
|
98
|
+
return;
|
|
99
|
+
settled = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
signalTree(child, "SIGKILL");
|
|
102
|
+
releaseChildHandles(child);
|
|
103
|
+
if (err !== null)
|
|
104
|
+
reject(err);
|
|
105
|
+
else
|
|
106
|
+
resolve(trust);
|
|
107
|
+
};
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
finish(new Error(`codex hook trust lookup timed out after ${timeoutMs} ms`));
|
|
110
|
+
}, timeoutMs);
|
|
111
|
+
timer.unref?.();
|
|
112
|
+
const onLine = (line) => {
|
|
113
|
+
let msg;
|
|
114
|
+
try {
|
|
115
|
+
msg = JSON.parse(line);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return; // A non-JSON-RPC line (a notice, a prompt) is not our reply.
|
|
119
|
+
}
|
|
120
|
+
if (typeof msg !== "object" || msg === null)
|
|
121
|
+
return;
|
|
122
|
+
const m = msg;
|
|
123
|
+
if (m.error !== undefined) {
|
|
124
|
+
finish(new Error(`codex hooks/list errored: ${describeError(m.error)}`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (m.id !== 2)
|
|
128
|
+
return;
|
|
129
|
+
const trust = findTrust(m.result, opts.definition);
|
|
130
|
+
if (trust !== null)
|
|
131
|
+
finish(null, trust);
|
|
132
|
+
else
|
|
133
|
+
finish(new Error("codex listed no session-flag PreToolUse hook with this command"));
|
|
134
|
+
};
|
|
135
|
+
child.stdout?.setEncoding("utf8");
|
|
136
|
+
child.stdout?.on("data", (chunk) => {
|
|
137
|
+
buffer += chunk;
|
|
138
|
+
let nl = buffer.indexOf("\n");
|
|
139
|
+
while (nl >= 0) {
|
|
140
|
+
const line = buffer.slice(0, nl);
|
|
141
|
+
buffer = buffer.slice(nl + 1);
|
|
142
|
+
if (line.trim() !== "")
|
|
143
|
+
onLine(line);
|
|
144
|
+
nl = buffer.indexOf("\n");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
child.on("error", (err) => finish(err instanceof Error ? err : new Error(String(err))));
|
|
148
|
+
child.on("close", (code) => {
|
|
149
|
+
if (!settled) {
|
|
150
|
+
finish(new Error(`codex app-server exited (code ${code ?? "signal"}) before answering hooks/list`));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
for (const message of hookListMessages(opts.cwd)) {
|
|
154
|
+
try {
|
|
155
|
+
child.stdin?.write(JSON.stringify(message) + "\n");
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// The pipe is gone (a failed spawn); the error handler settles the lookup.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** The trimmed output of `<bin> --version`, or "" when the probe fails or prints nothing. It
|
|
164
|
+
* keys the trust cache, so a codex upgrade re-looks-up the guard's trust for the new build.
|
|
165
|
+
* Async (spawn, not spawnSync): a hung binary's probe must not block the event loop — every
|
|
166
|
+
* run's `prepare` awaits this, and a blocking probe would freeze the whole agent while waiting
|
|
167
|
+
* out its deadline. The child's group is killed whether it succeeds or times out. */
|
|
168
|
+
function codexVersion(bin, env, cwd, timeoutMs) {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const child = spawn(bin, ["--version"], {
|
|
171
|
+
env,
|
|
172
|
+
cwd,
|
|
173
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
174
|
+
});
|
|
175
|
+
let out = "";
|
|
176
|
+
let done = false;
|
|
177
|
+
const finish = (value) => {
|
|
178
|
+
if (done)
|
|
179
|
+
return;
|
|
180
|
+
done = true;
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
signalTree(child, "SIGKILL");
|
|
183
|
+
releaseChildHandles(child);
|
|
184
|
+
resolve(value);
|
|
185
|
+
};
|
|
186
|
+
const timer = setTimeout(() => finish(""), timeoutMs);
|
|
187
|
+
timer.unref?.();
|
|
188
|
+
child.stdout?.setEncoding("utf8");
|
|
189
|
+
child.stdout?.on("data", (chunk) => {
|
|
190
|
+
out += chunk;
|
|
191
|
+
});
|
|
192
|
+
child.on("error", () => finish(""));
|
|
193
|
+
child.on("close", () => finish(out.trim()));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Whether a cached value is a usable `{key, hash}` entry. */
|
|
197
|
+
function isTrust(value) {
|
|
198
|
+
return (typeof value === "object" &&
|
|
199
|
+
value !== null &&
|
|
200
|
+
typeof value.key === "string" &&
|
|
201
|
+
typeof value.hash === "string");
|
|
202
|
+
}
|
|
203
|
+
/** Resolve the guard hook's trust, cached per codex version and hook definition in
|
|
204
|
+
* `.tumwater/state/codex-hook-trust.json` so a run's argv never spawns `app-server` again.
|
|
205
|
+
* Returns the trust, rejecting on a failed lookup.
|
|
206
|
+
*
|
|
207
|
+
* The cache key is the trimmed `<bin> --version` output plus `JSON.stringify(definition)`, so
|
|
208
|
+
* a changed codex version, a changed hook command, or a changed `timeout` (which changes the
|
|
209
|
+
* hash, finding 3) each re-look-up; each entry is `{key, hash, checkedAt}`. A hit resolves
|
|
210
|
+
* without spawning `app-server`; a miss looks up and writes the entry atomically.
|
|
211
|
+
*
|
|
212
|
+
* `root` undefined (a hand run with no `.tumwater` root) looks up and caches nothing and never
|
|
213
|
+
* creates a `.tumwater` directory — a hand run once minted a phantom `$TMPDIR/.tumwater` and
|
|
214
|
+
* reddened every suite on the host (plans/codex-write-guard.md "Visibility"). */
|
|
215
|
+
export async function codexHookTrust(root, opts) {
|
|
216
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
217
|
+
if (root === undefined)
|
|
218
|
+
return lookupCodexHookTrust(opts);
|
|
219
|
+
const file = codexHookTrustPath(root);
|
|
220
|
+
// The version probe and the app-server lookup share one deadline (timeoutMs): a failed
|
|
221
|
+
// lookup — a hung binary, an app-server that never answers — never delays a run past the
|
|
222
|
+
// bound, and the probe never blocks the event loop (it is async, not spawnSync). The probe
|
|
223
|
+
// is a quick identity read, so its own slice is capped at a third of the deadline — a slow
|
|
224
|
+
// probe never starves the app-server lookup of its budget, and the sum of the two always
|
|
225
|
+
// stays within the one deadline.
|
|
226
|
+
const deadline = Date.now() + timeoutMs;
|
|
227
|
+
const version = await codexVersion(opts.bin, opts.env, opts.cwd, Math.floor(timeoutMs / 3));
|
|
228
|
+
const cacheKey = `${version}${JSON.stringify(opts.definition)}`;
|
|
229
|
+
const cache = readJsonFile(file);
|
|
230
|
+
const hit = cache?.[cacheKey];
|
|
231
|
+
if (isTrust(hit))
|
|
232
|
+
return { key: hit.key, hash: hit.hash };
|
|
233
|
+
const trust = await lookupCodexHookTrust({
|
|
234
|
+
...opts,
|
|
235
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
236
|
+
});
|
|
237
|
+
const next = { ...(cache ?? {}) };
|
|
238
|
+
next[cacheKey] = { key: trust.key, hash: trust.hash, checkedAt: Date.now() };
|
|
239
|
+
try {
|
|
240
|
+
writeJsonAtomic(file, next);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// A cache write failure must never fail the lookup whose result it is only persisting.
|
|
244
|
+
}
|
|
245
|
+
return trust;
|
|
246
|
+
}
|