tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ and is also outside `npm test`.
|
|
|
42
42
|
## Status
|
|
43
43
|
|
|
44
44
|
<!-- tumwater:status:start -->
|
|
45
|
-
**v0.4.
|
|
45
|
+
**v0.4.2**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
|
|
46
46
|
which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
|
|
47
47
|
issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
|
|
48
48
|
|
|
@@ -75,11 +75,9 @@ and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater
|
|
|
75
75
|
so pair it with `--token <secret>`.
|
|
76
76
|
|
|
77
77
|
`tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
|
|
78
|
-
model cannot read images, the command refuses the attachment up front (naming the fix)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
|
|
82
|
-
run reroutes.
|
|
78
|
+
model cannot read images, the command refuses the attachment up front (naming the fix); the
|
|
79
|
+
dashboards and `tumwater role <id>` say which roles read images. Nothing reroutes a blind read
|
|
80
|
+
to another tier — a run that reads an image its model cannot see fails with `lastError`.
|
|
83
81
|
|
|
84
82
|
Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
|
|
85
83
|
same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
|
package/dist/build-info.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** The backend seam (plans/agent-backends.md "The seam"): what one non-interactive agent
|
|
2
2
|
* run needs from its backend — how to build the command, how to translate its native stdout
|
|
3
|
-
* into tumwater's canonical event stream, and how to classify its exit. src/agent/run-
|
|
4
|
-
*
|
|
3
|
+
* into tumwater's canonical event stream, and how to classify its exit. src/agent/run-spawn.ts
|
|
4
|
+
* builds and spawns the run (the marker, parser, bin resolution and spawn); src/agent/run-agent.ts
|
|
5
|
+
* keeps the coordinator — the backend-neutral watchdogs, the abort wiring and the settlement-state
|
|
6
|
+
* assembly — while src/agent/run-stream.ts owns the streaming pipeline (native-to-canonical stdout
|
|
7
|
+
* split and the stderr window) and the settle/flush; the
|
|
5
8
|
* post-run settlement — the group-kill and marker sweeps on exit, the spawn-error result and
|
|
6
9
|
* the close-time reconcile/classify that builds the `PiRunResult` — lives in
|
|
7
10
|
* src/agent/run-settle.ts. A backend registered here supplies the rest. src/agent/run-agent.ts
|
|
@@ -88,3 +88,24 @@ export function backendDefaultLabel(backend) {
|
|
|
88
88
|
return `${backend} default`;
|
|
89
89
|
return "pi default";
|
|
90
90
|
}
|
|
91
|
+
/** The HTTP status codes the native claude and codex stream translators read as a transient
|
|
92
|
+
* backend (server) failure — the `transientBackend` classification with `backendKind:
|
|
93
|
+
* "server"`. 529 is Anthropic's "overloaded" spelling, which codex's `turn.failed` error
|
|
94
|
+
* carries too. One home for the set the two adapters' classifiers share, so a status added
|
|
95
|
+
* for one cannot go missing from the other (opencode classifies the same failure from message
|
|
96
|
+
* text, not a status code, so it is not in the set). */
|
|
97
|
+
export const SERVER_TRANSIENT_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
98
|
+
/** Classify a failure message's prose into the transient-backend failure it names, when the
|
|
99
|
+
* wire carries no structured signal: a dropped connection or a timeout, read from the message
|
|
100
|
+
* text by claude's and opencode's stream translators, which both fall back to prose for the
|
|
101
|
+
* statuses their native events leave to the message (claude's classifyResult and opencode's
|
|
102
|
+
* classifyError end with this same connection/then-timeout tail). The single home of the
|
|
103
|
+
* connection/timeout vocabulary, so the two backends' tail regexes cannot drift. undefined
|
|
104
|
+
* when the text names neither. */
|
|
105
|
+
export function transientProseKind(text) {
|
|
106
|
+
if (/connection|econn|refused|reset|terminated/i.test(text))
|
|
107
|
+
return { kind: "transientBackend", backendKind: "connection" };
|
|
108
|
+
if (/timed out|timeout/i.test(text))
|
|
109
|
+
return { kind: "transientBackend", backendKind: "timeout" };
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The claude adapter for the agent seam (plans/agent-backends.md "The seam", "Sessions",
|
|
2
2
|
* "The claude invocation"). It shapes one run's argv/stdin/env, translates claude's
|
|
3
3
|
* `stream-json` into the canonical stream, and classifies the exit's stderr — everything
|
|
4
|
-
* backend-specific. src/agent/run-
|
|
4
|
+
* backend-specific. src/agent/run-spawn.ts spawns the run and records this run in the session
|
|
5
5
|
* ledger; claude mints its own session id (a fresh UUID, or the resumed record's id), which
|
|
6
6
|
* `invocation` returns so the ledger can carry it.
|
|
7
7
|
*
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
* Pure translation: no spawn, no backend registration, no config. The factory keeps per-run
|
|
12
12
|
* state (the pending split-block assistant message, the unmapped native types seen, whether a
|
|
13
13
|
* `run_cost` arrived), so call it once per run. Unmapped native types are recorded once each
|
|
14
|
-
* on the seam's optional `warnings()` — src/agent/run-
|
|
14
|
+
* on the seam's optional `warnings()` — src/agent/run-stream.ts forwards each as an
|
|
15
15
|
* `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
|
|
16
16
|
* run produced no authoritative `run_cost`. */
|
|
17
17
|
import { joinParts } from "../../text/text.js";
|
|
18
|
-
import {
|
|
19
|
-
import { canonicalUsageBlock, isJsonObject, objectOrEmpty,
|
|
18
|
+
import { secondsUntilUnixStamp } from "../../text/datetime.js";
|
|
19
|
+
import { canonicalUsageBlock, isJsonObject, nonNegativeNumber, objectOrEmpty, optionalString, parseJsonObject } from "../../files/json-object.js";
|
|
20
|
+
import { SERVER_TRANSIENT_STATUS, transientProseKind } from "../backend-traits.js";
|
|
20
21
|
/** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
|
|
21
22
|
* lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
|
|
22
23
|
* lower-cased so a future tool still reads as a name rather than as `undefined`. */
|
|
@@ -29,14 +30,8 @@ const TOOL_NAMES = {
|
|
|
29
30
|
Glob: "glob",
|
|
30
31
|
Grep: "grep",
|
|
31
32
|
};
|
|
32
|
-
/** claude's HTTP status classes that mean a transient backend failure. 529 is Anthropic's
|
|
33
|
-
* "overloaded" spelling. */
|
|
34
|
-
const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
|
|
35
33
|
/** claude's HTTP status classes that mean a permanent configuration error. */
|
|
36
34
|
const CONFIG_STATUS = new Set([400, 401, 403, 404]);
|
|
37
|
-
function str(value) {
|
|
38
|
-
return typeof value === "string" ? value : undefined;
|
|
39
|
-
}
|
|
40
35
|
/** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
|
|
41
36
|
* authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
|
|
42
37
|
* on every event, so summing per event would double-count — the fold takes it once). The
|
|
@@ -51,15 +46,15 @@ function canonicalUsage(usage) {
|
|
|
51
46
|
function canonicalBlock(block) {
|
|
52
47
|
switch (block.type) {
|
|
53
48
|
case "text":
|
|
54
|
-
return { type: "text", text:
|
|
49
|
+
return { type: "text", text: optionalString(block.text) ?? "" };
|
|
55
50
|
case "thinking":
|
|
56
|
-
return { type: "thinking", thinking:
|
|
51
|
+
return { type: "thinking", thinking: optionalString(block.thinking) ?? optionalString(block.text) ?? "" };
|
|
57
52
|
case "tool_use": {
|
|
58
53
|
const input = objectOrEmpty(block.input);
|
|
59
54
|
const args = { ...input };
|
|
60
55
|
if (typeof input.file_path === "string")
|
|
61
56
|
args.path = input.file_path;
|
|
62
|
-
const name =
|
|
57
|
+
const name = optionalString(block.name) ?? "";
|
|
63
58
|
// Own-property test, not a bare index: a tool name like `constructor`, `toString`, or
|
|
64
59
|
// `__proto__` would otherwise inherit a function (or Object.prototype) from the table's
|
|
65
60
|
// prototype chain and bypass the lower-cased fallback — JSON.stringify then drops the
|
|
@@ -67,7 +62,7 @@ function canonicalBlock(block) {
|
|
|
67
62
|
// applies to EFFORT_BY_THINKING.
|
|
68
63
|
return {
|
|
69
64
|
type: "toolCall",
|
|
70
|
-
id:
|
|
65
|
+
id: optionalString(block.id) ?? "",
|
|
71
66
|
name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
|
|
72
67
|
arguments: args,
|
|
73
68
|
};
|
|
@@ -83,18 +78,18 @@ function canonicalBlock(block) {
|
|
|
83
78
|
* provider classification applies. `status` is `api_error_status`, which is null on the
|
|
84
79
|
* not-logged-in result, so the text checks carry that case. */
|
|
85
80
|
function classifyResult(result) {
|
|
86
|
-
const terminal =
|
|
81
|
+
const terminal = optionalString(result.terminal_reason);
|
|
87
82
|
if (terminal === "aborted_tools")
|
|
88
83
|
return {};
|
|
89
84
|
if (terminal === "error_max_budget_usd")
|
|
90
85
|
return { kind: "budgetCapped" };
|
|
91
|
-
const text = joinParts([
|
|
86
|
+
const text = joinParts([optionalString(result.result), optionalString(result.error)], " ");
|
|
92
87
|
if (/error_max_budget_usd/i.test(text))
|
|
93
88
|
return { kind: "budgetCapped" };
|
|
94
89
|
const status = nonNegativeNumber(result.api_error_status, undefined);
|
|
95
90
|
if (status === 429)
|
|
96
91
|
return { kind: "transientRateLimit" };
|
|
97
|
-
if (status !== undefined &&
|
|
92
|
+
if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
|
|
98
93
|
return { kind: "transientBackend", backendKind: "server" };
|
|
99
94
|
// The context-overflow text wins over a configuration status: claude reports an oversized
|
|
100
95
|
// prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
|
|
@@ -111,10 +106,9 @@ function classifyResult(result) {
|
|
|
111
106
|
return { kind: "transientRateLimit" };
|
|
112
107
|
if (/overloaded|internal server error|bad gateway|service unavailable|gateway timeout|\b5\d\d\b/i.test(text))
|
|
113
108
|
return { kind: "transientBackend", backendKind: "server" };
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return { kind: "transientBackend", backendKind: "timeout" };
|
|
109
|
+
const prose = transientProseKind(text);
|
|
110
|
+
if (prose)
|
|
111
|
+
return prose;
|
|
118
112
|
if (/\b(400|401|403|404)\b/i.test(text))
|
|
119
113
|
return { kind: "configError" };
|
|
120
114
|
return {};
|
|
@@ -170,174 +164,179 @@ export function claudeTranslator(opts = {}) {
|
|
|
170
164
|
seen.add(kind);
|
|
171
165
|
warnings.add(kind);
|
|
172
166
|
};
|
|
167
|
+
const handleSystem = (event) => {
|
|
168
|
+
const subtype = optionalString(event.subtype);
|
|
169
|
+
if (subtype === "init") {
|
|
170
|
+
const out = [];
|
|
171
|
+
const cwd = optionalString(event.cwd);
|
|
172
|
+
const id = optionalString(event.session_id);
|
|
173
|
+
out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
|
|
174
|
+
out.push(JSON.stringify({ type: "agent_start" }));
|
|
175
|
+
const billing = billingOf(optionalString(event.apiKeySource), opts.thirdPartyProvider === true);
|
|
176
|
+
if (billing)
|
|
177
|
+
out.push(JSON.stringify({ type: "billing", billing }));
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
if (subtype === "api_retry") {
|
|
181
|
+
const attempt = nonNegativeNumber(event.attempt, undefined);
|
|
182
|
+
const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
|
|
183
|
+
const status = nonNegativeNumber(event.error_status, undefined);
|
|
184
|
+
const error = optionalString(event.error) ?? "";
|
|
185
|
+
const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
|
|
186
|
+
return [
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
type: "auto_retry_start",
|
|
189
|
+
...(attempt !== undefined ? { attempt } : {}),
|
|
190
|
+
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
191
|
+
errorMessage: errorMessage || "api retry",
|
|
192
|
+
}),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
if (subtype === "compact_boundary")
|
|
196
|
+
return [JSON.stringify({ type: "compaction_start" })];
|
|
197
|
+
// A thinking-token estimate during extended thinking: known, carries nothing canonical.
|
|
198
|
+
if (subtype === "thinking_tokens")
|
|
199
|
+
return [];
|
|
200
|
+
record(`system:${subtype ?? "unknown"}`);
|
|
201
|
+
return [];
|
|
202
|
+
};
|
|
203
|
+
const handleAssistant = (event) => {
|
|
204
|
+
sawActivity = true;
|
|
205
|
+
const message = objectOrEmpty(event.message);
|
|
206
|
+
const id = optionalString(message.id) ?? `anon-${anonymous++}`;
|
|
207
|
+
// A new id flushes the open message; a continuing one flushes nothing. Either way the
|
|
208
|
+
// fresh or open pending then folds this event's content, usage, and stop reason the
|
|
209
|
+
// same way — that shared fold is why both branches live on one path.
|
|
210
|
+
let out = [];
|
|
211
|
+
if (pending?.id !== id) {
|
|
212
|
+
out = flush();
|
|
213
|
+
pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
|
|
214
|
+
}
|
|
215
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
216
|
+
for (const raw of blocks) {
|
|
217
|
+
if (!isJsonObject(raw))
|
|
218
|
+
continue;
|
|
219
|
+
const block = canonicalBlock(raw);
|
|
220
|
+
if (!block)
|
|
221
|
+
continue;
|
|
222
|
+
pending.content.push(block);
|
|
223
|
+
if (block.type === "toolCall")
|
|
224
|
+
out.push(startToolCall(block));
|
|
225
|
+
}
|
|
226
|
+
// Usage repeats on every event of the same message; the last read wins and is counted
|
|
227
|
+
// once because the fold emits one message_end per id.
|
|
228
|
+
pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
|
|
229
|
+
const stopReason = optionalString(message.stop_reason);
|
|
230
|
+
if (stopReason !== undefined)
|
|
231
|
+
pending.stopReason = stopReason;
|
|
232
|
+
return out;
|
|
233
|
+
};
|
|
234
|
+
const handleUser = (event) => {
|
|
235
|
+
const out = flush();
|
|
236
|
+
const message = objectOrEmpty(event.message);
|
|
237
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
238
|
+
for (const raw of blocks) {
|
|
239
|
+
if (!isJsonObject(raw) || raw.type !== "tool_result")
|
|
240
|
+
continue;
|
|
241
|
+
out.push(JSON.stringify({
|
|
242
|
+
type: "tool_execution_end",
|
|
243
|
+
toolCallId: optionalString(raw.tool_use_id) ?? "",
|
|
244
|
+
isError: raw.is_error === true,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
const handleRateLimitEvent = (event) => {
|
|
250
|
+
const info = objectOrEmpty(event.rate_limit_info);
|
|
251
|
+
const rateLimitType = optionalString(info.rateLimitType);
|
|
252
|
+
const status = optionalString(info.status);
|
|
253
|
+
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
254
|
+
// keyed by window type, and only the binding window's status at the top level.
|
|
255
|
+
const windows = [];
|
|
256
|
+
for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
|
|
257
|
+
if (type === rateLimitType || !isJsonObject(raw))
|
|
258
|
+
continue;
|
|
259
|
+
const reading = { type: "rate_limit", rateLimitType: type };
|
|
260
|
+
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
261
|
+
if (windowResets !== undefined)
|
|
262
|
+
reading.resetsAt = windowResets;
|
|
263
|
+
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
264
|
+
if (windowUtil !== undefined)
|
|
265
|
+
reading.utilization = windowUtil;
|
|
266
|
+
windows.push(JSON.stringify(reading));
|
|
267
|
+
}
|
|
268
|
+
const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
269
|
+
const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
270
|
+
const out = { type: "rate_limit" };
|
|
271
|
+
if (rateLimitType !== undefined)
|
|
272
|
+
out.rateLimitType = rateLimitType;
|
|
273
|
+
if (status !== undefined)
|
|
274
|
+
out.status = status;
|
|
275
|
+
if (resetsAt !== undefined)
|
|
276
|
+
out.resetsAt = resetsAt;
|
|
277
|
+
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
278
|
+
if (utilization !== undefined)
|
|
279
|
+
out.utilization = utilization;
|
|
280
|
+
const overageStatus = optionalString(info.overageStatus);
|
|
281
|
+
if (overageStatus !== undefined)
|
|
282
|
+
out.overageStatus = overageStatus;
|
|
283
|
+
if (info.isUsingOverage === true)
|
|
284
|
+
out.isUsingOverage = true;
|
|
285
|
+
if (status === "rejected" && resetsAt !== undefined) {
|
|
286
|
+
// resetsAt is a unix-seconds stamp; secondsUntilUnixStamp folds it to whole seconds.
|
|
287
|
+
out.retryAfterSeconds = secondsUntilUnixStamp(resetsAt, Date.now());
|
|
288
|
+
}
|
|
289
|
+
return [JSON.stringify(out), ...windows];
|
|
290
|
+
};
|
|
291
|
+
const handleResult = (event) => {
|
|
292
|
+
sawActivity = true;
|
|
293
|
+
const out = flush();
|
|
294
|
+
// The assistant events' usage is the message-start snapshot, so the result's total
|
|
295
|
+
// is the run's real output-token count (thinking included).
|
|
296
|
+
const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
|
|
297
|
+
if (outputTokens !== undefined)
|
|
298
|
+
out.push(JSON.stringify({ type: "run_usage", outputTokens }));
|
|
299
|
+
const total = nonNegativeNumber(event.total_cost_usd, undefined);
|
|
300
|
+
if (total !== undefined) {
|
|
301
|
+
sawRunCost = true;
|
|
302
|
+
out.push(JSON.stringify({ type: "run_cost", total }));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
warnings.add("cost_unknown");
|
|
306
|
+
}
|
|
307
|
+
if (event.is_error === true) {
|
|
308
|
+
const { kind, backendKind } = classifyResult(event);
|
|
309
|
+
const message = {
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: [],
|
|
312
|
+
stopReason: "error",
|
|
313
|
+
};
|
|
314
|
+
const errorMessage = optionalString(event.result) ?? optionalString(event.error);
|
|
315
|
+
if (errorMessage !== undefined)
|
|
316
|
+
message.errorMessage = errorMessage;
|
|
317
|
+
// tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
|
|
318
|
+
// reads event.tumwaterKind and trusts it before its text regexes.
|
|
319
|
+
out.push(JSON.stringify({
|
|
320
|
+
type: "message_end",
|
|
321
|
+
message,
|
|
322
|
+
...(kind !== undefined ? { tumwaterKind: kind } : {}),
|
|
323
|
+
...(backendKind !== undefined ? { backendKind } : {}),
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
};
|
|
173
328
|
const translate = (line) => {
|
|
174
329
|
const parsed = parseJsonObject(line);
|
|
175
330
|
if (!parsed)
|
|
176
331
|
return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
|
|
177
332
|
const event = parsed;
|
|
178
|
-
const type =
|
|
333
|
+
const type = optionalString(event.type);
|
|
179
334
|
switch (type) {
|
|
180
|
-
case "system":
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const id = str(event.session_id);
|
|
186
|
-
out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
|
|
187
|
-
out.push(JSON.stringify({ type: "agent_start" }));
|
|
188
|
-
const billing = billingOf(str(event.apiKeySource), opts.thirdPartyProvider === true);
|
|
189
|
-
if (billing)
|
|
190
|
-
out.push(JSON.stringify({ type: "billing", billing }));
|
|
191
|
-
return out;
|
|
192
|
-
}
|
|
193
|
-
if (subtype === "api_retry") {
|
|
194
|
-
const attempt = nonNegativeNumber(event.attempt, undefined);
|
|
195
|
-
const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
|
|
196
|
-
const status = nonNegativeNumber(event.error_status, undefined);
|
|
197
|
-
const error = str(event.error) ?? "";
|
|
198
|
-
const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
|
|
199
|
-
return [
|
|
200
|
-
JSON.stringify({
|
|
201
|
-
type: "auto_retry_start",
|
|
202
|
-
...(attempt !== undefined ? { attempt } : {}),
|
|
203
|
-
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
204
|
-
errorMessage: errorMessage || "api retry",
|
|
205
|
-
}),
|
|
206
|
-
];
|
|
207
|
-
}
|
|
208
|
-
if (subtype === "compact_boundary")
|
|
209
|
-
return [JSON.stringify({ type: "compaction_start" })];
|
|
210
|
-
// A thinking-token estimate during extended thinking: known, carries nothing canonical.
|
|
211
|
-
if (subtype === "thinking_tokens")
|
|
212
|
-
return [];
|
|
213
|
-
record(`system:${subtype ?? "unknown"}`);
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
|
-
case "assistant": {
|
|
217
|
-
sawActivity = true;
|
|
218
|
-
const message = objectOrEmpty(event.message);
|
|
219
|
-
const id = str(message.id) ?? `anon-${anonymous++}`;
|
|
220
|
-
// A new id flushes the open message; a continuing one flushes nothing. Either way the
|
|
221
|
-
// fresh or open pending then folds this event's content, usage, and stop reason the
|
|
222
|
-
// same way — that shared fold is why both branches live on one path.
|
|
223
|
-
let out = [];
|
|
224
|
-
if (pending?.id !== id) {
|
|
225
|
-
out = flush();
|
|
226
|
-
pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
|
|
227
|
-
}
|
|
228
|
-
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
229
|
-
for (const raw of blocks) {
|
|
230
|
-
if (!isJsonObject(raw))
|
|
231
|
-
continue;
|
|
232
|
-
const block = canonicalBlock(raw);
|
|
233
|
-
if (!block)
|
|
234
|
-
continue;
|
|
235
|
-
pending.content.push(block);
|
|
236
|
-
if (block.type === "toolCall")
|
|
237
|
-
out.push(startToolCall(block));
|
|
238
|
-
}
|
|
239
|
-
// Usage repeats on every event of the same message; the last read wins and is counted
|
|
240
|
-
// once because the fold emits one message_end per id.
|
|
241
|
-
pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
|
|
242
|
-
const stopReason = str(message.stop_reason);
|
|
243
|
-
if (stopReason !== undefined)
|
|
244
|
-
pending.stopReason = stopReason;
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
case "user": {
|
|
248
|
-
const out = flush();
|
|
249
|
-
const message = objectOrEmpty(event.message);
|
|
250
|
-
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
251
|
-
for (const raw of blocks) {
|
|
252
|
-
if (!isJsonObject(raw) || raw.type !== "tool_result")
|
|
253
|
-
continue;
|
|
254
|
-
out.push(JSON.stringify({
|
|
255
|
-
type: "tool_execution_end",
|
|
256
|
-
toolCallId: str(raw.tool_use_id) ?? "",
|
|
257
|
-
isError: raw.is_error === true,
|
|
258
|
-
}));
|
|
259
|
-
}
|
|
260
|
-
return out;
|
|
261
|
-
}
|
|
262
|
-
case "rate_limit_event": {
|
|
263
|
-
const info = objectOrEmpty(event.rate_limit_info);
|
|
264
|
-
const rateLimitType = str(info.rateLimitType);
|
|
265
|
-
const status = str(info.status);
|
|
266
|
-
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
267
|
-
// keyed by window type, and only the binding window's status at the top level.
|
|
268
|
-
const windows = [];
|
|
269
|
-
for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
|
|
270
|
-
if (type === rateLimitType || !isJsonObject(raw))
|
|
271
|
-
continue;
|
|
272
|
-
const reading = { type: "rate_limit", rateLimitType: type };
|
|
273
|
-
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
274
|
-
if (windowResets !== undefined)
|
|
275
|
-
reading.resetsAt = windowResets;
|
|
276
|
-
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
277
|
-
if (windowUtil !== undefined)
|
|
278
|
-
reading.utilization = windowUtil;
|
|
279
|
-
windows.push(JSON.stringify(reading));
|
|
280
|
-
}
|
|
281
|
-
const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
282
|
-
const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
283
|
-
const out = { type: "rate_limit" };
|
|
284
|
-
if (rateLimitType !== undefined)
|
|
285
|
-
out.rateLimitType = rateLimitType;
|
|
286
|
-
if (status !== undefined)
|
|
287
|
-
out.status = status;
|
|
288
|
-
if (resetsAt !== undefined)
|
|
289
|
-
out.resetsAt = resetsAt;
|
|
290
|
-
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
291
|
-
if (utilization !== undefined)
|
|
292
|
-
out.utilization = utilization;
|
|
293
|
-
const overageStatus = str(info.overageStatus);
|
|
294
|
-
if (overageStatus !== undefined)
|
|
295
|
-
out.overageStatus = overageStatus;
|
|
296
|
-
if (info.isUsingOverage === true)
|
|
297
|
-
out.isUsingOverage = true;
|
|
298
|
-
if (status === "rejected" && resetsAt !== undefined) {
|
|
299
|
-
// resetsAt is a unix-seconds stamp; secondsUntil folds it (×1000) to whole seconds.
|
|
300
|
-
out.retryAfterSeconds = secondsUntil(resetsAt * 1000, Date.now());
|
|
301
|
-
}
|
|
302
|
-
return [JSON.stringify(out), ...windows];
|
|
303
|
-
}
|
|
304
|
-
case "result": {
|
|
305
|
-
sawActivity = true;
|
|
306
|
-
const out = flush();
|
|
307
|
-
// The assistant events' usage is the message-start snapshot, so the result's total
|
|
308
|
-
// is the run's real output-token count (thinking included).
|
|
309
|
-
const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
|
|
310
|
-
if (outputTokens !== undefined)
|
|
311
|
-
out.push(JSON.stringify({ type: "run_usage", outputTokens }));
|
|
312
|
-
const total = nonNegativeNumber(event.total_cost_usd, undefined);
|
|
313
|
-
if (total !== undefined) {
|
|
314
|
-
sawRunCost = true;
|
|
315
|
-
out.push(JSON.stringify({ type: "run_cost", total }));
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
warnings.add("cost_unknown");
|
|
319
|
-
}
|
|
320
|
-
if (event.is_error === true) {
|
|
321
|
-
const { kind, backendKind } = classifyResult(event);
|
|
322
|
-
const message = {
|
|
323
|
-
role: "assistant",
|
|
324
|
-
content: [],
|
|
325
|
-
stopReason: "error",
|
|
326
|
-
};
|
|
327
|
-
const errorMessage = str(event.result) ?? str(event.error);
|
|
328
|
-
if (errorMessage !== undefined)
|
|
329
|
-
message.errorMessage = errorMessage;
|
|
330
|
-
// tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
|
|
331
|
-
// reads event.tumwaterKind and trusts it before its text regexes.
|
|
332
|
-
out.push(JSON.stringify({
|
|
333
|
-
type: "message_end",
|
|
334
|
-
message,
|
|
335
|
-
...(kind !== undefined ? { tumwaterKind: kind } : {}),
|
|
336
|
-
...(backendKind !== undefined ? { backendKind } : {}),
|
|
337
|
-
}));
|
|
338
|
-
}
|
|
339
|
-
return out;
|
|
340
|
-
}
|
|
335
|
+
case "system": return handleSystem(event);
|
|
336
|
+
case "assistant": return handleAssistant(event);
|
|
337
|
+
case "user": return handleUser(event);
|
|
338
|
+
case "rate_limit_event": return handleRateLimitEvent(event);
|
|
339
|
+
case "result": return handleResult(event);
|
|
341
340
|
case "tool_progress":
|
|
342
341
|
// A 30 s heartbeat of a running tool call: known, and the open call already tracks
|
|
343
342
|
// liveness.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The PreToolUse hook claude runs before each file write in a team run (claude-write-guard.ts
|
|
2
|
-
* installs it through `--settings`): `node write-guard-hook.js <worktree>` with
|
|
2
|
+
* installs it through `--settings`): `node claude-write-guard-hook.js <worktree>` with the hook
|
|
3
3
|
* payload on stdin. To block the write it prints a `deny` decision with the refusal as its reason:
|
|
4
4
|
* the model reads only the reason, where a refusal by exit 2 would also quote the whole hook
|
|
5
5
|
* command. It prints nothing to let the write run. Any failure of its own lets the write run, so a
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `--setting-sources project` leaves the operator's own hooks out of a run, while `--settings`
|
|
8
8
|
* still loads this one. The hook gets the run's worktree as an argument rather than reading
|
|
9
9
|
* claude's `cwd`, which follows the Bash tool's `cd` into subdirectories. The entry point is
|
|
10
|
-
* write-guard-hook.ts beside this file. */
|
|
10
|
+
* claude-write-guard-hook.ts beside this file. */
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { shellQuote, strayWriteRefusal } from "../write-guard.js";
|
|
13
13
|
/** claude's file-writing tools a team run has: claude-args.ts's `--tools` allows only these two,
|
|
@@ -16,12 +16,13 @@ const WRITE_TOOLS = ["Edit", "Write"];
|
|
|
16
16
|
/** Seconds claude waits for the hook before running the tool anyway. */
|
|
17
17
|
const HOOK_TIMEOUT_SECONDS = 30;
|
|
18
18
|
/** The `--settings` value for a run in `cwd`: one PreToolUse hook on the file-writing tools that
|
|
19
|
-
* runs the compiled write-guard-hook.js with this process's node, falling back to
|
|
20
|
-
* when that binary has moved (as the notes shim does), and the run's worktree
|
|
19
|
+
* runs the compiled claude-write-guard-hook.js with this process's node, falling back to
|
|
20
|
+
* `node` on PATH when that binary has moved (as the notes shim does), and the run's worktree
|
|
21
|
+
* as its argument. The
|
|
21
22
|
* hook path resolves from this module, so a staged build (.tumwater/build/<sha>) runs its own
|
|
22
23
|
* copy. */
|
|
23
24
|
export function writeGuardSettings(cwd, nodeBin = process.execPath) {
|
|
24
|
-
const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
|
|
25
|
+
const hook = fileURLToPath(new URL("./claude-write-guard-hook.js", import.meta.url));
|
|
25
26
|
const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)} ${shellQuote(cwd)}`;
|
|
26
27
|
return JSON.stringify({
|
|
27
28
|
hooks: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The codex adapter for the agent seam (plans/codex-backend.md §4, §5, §6): it shapes one
|
|
2
2
|
* run's argv/stdin/env, translates codex's `exec --json` stream, reads the authoritative
|
|
3
3
|
* rollout after the process exits, and classifies the exit's stderr — everything
|
|
4
|
-
* backend-specific. src/agent/run-
|
|
4
|
+
* backend-specific. src/agent/run-spawn.ts spawns the run and records this run in the session
|
|
5
5
|
* ledger; codex mints its own session id (a thread id, or the resumed record's id) in the
|
|
6
6
|
* stream's `thread.started`, which the runner's stream-mint pass appends to the ledger and
|
|
7
7
|
* `invocation` returns so a later `exec resume` can reach it.
|