tumwater 0.4.0 → 0.4.1
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 +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +7 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +169 -166
- 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 +10 -21
- package/dist/src/agent/codex/codex-stream.js +199 -178
- 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/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +97 -88
- package/dist/src/agent/run-agent.js +66 -381
- package/dist/src/agent/run-settle.js +3 -2
- package/dist/src/agent/run-spawn.js +235 -0
- package/dist/src/agent/run-stream.js +193 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +28 -11
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +1 -1
- 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 +1 -1
- package/dist/src/backlog/backlog-release.js +10 -17
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +0 -144
- package/dist/src/backlog/backlog-write.js +30 -17
- package/dist/src/backlog/backlog.js +9 -0
- package/dist/src/backlog/question-answer.js +3 -1
- package/dist/src/baseline/main-baseline.js +50 -15
- package/dist/src/baseline/main-guardrails.js +29 -5
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +8 -2
- 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 -6
- package/dist/src/budget/budget.js +1 -1
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-flag-specs.js +5 -0
- 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-role-command.js +26 -49
- package/dist/src/cli/cli-start.js +65 -39
- package/dist/src/cli/help.js +13 -5
- package/dist/src/cli/log-commands.js +125 -106
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +399 -342
- package/dist/src/collections.js +26 -3
- 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-write.js +194 -157
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +14 -14
- package/dist/src/doctor/doctor-backlog-plans.js +17 -18
- package/dist/src/doctor/doctor-backlog.js +19 -13
- package/dist/src/doctor/doctor-check-names.js +2 -0
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +51 -18
- package/dist/src/doctor/doctor-holds.js +9 -9
- package/dist/src/doctor/doctor-notify.js +1 -1
- package/dist/src/doctor/doctor-opencode-checks.js +16 -4
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +4 -3
- package/dist/src/doctor/doctor-role-notes.js +51 -0
- package/dist/src/doctor/doctor.js +7 -0
- package/dist/src/events/event-format.js +309 -154
- 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 +174 -8
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- 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/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +4 -5
- 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 +1 -1
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +19 -10
- package/dist/src/gui/gui-backlog-endpoints.js +1 -1
- 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 -5
- package/dist/src/gui/gui-prompt-commands.js +1 -1
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +11 -8
- package/dist/src/gui/gui-server.js +5 -2
- 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 +2 -2
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +189 -161
- package/dist/src/init/init.js +43 -17
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- 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 +16 -5
- package/dist/src/operator/operator-intent.js +26 -3
- 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/paths.js +10 -4
- 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 +65 -49
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +1 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +7 -4
- package/dist/src/prompt/gate-notes.js +34 -9
- package/dist/src/prompt/notes-backlog.js +11 -5
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt.js +11 -7
- 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 +298 -179
- package/dist/src/report/report-fold.js +251 -184
- package/dist/src/report/report-main-red.js +64 -0
- package/dist/src/report/report-render.js +127 -21
- package/dist/src/review/known-flakes.js +20 -1
- package/dist/src/review/post-merge-review.js +11 -9
- package/dist/src/review/quarantine-render.js +27 -10
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +14 -1
- package/dist/src/roles/role-guidance.js +8 -0
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +18 -3
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +27 -3
- package/dist/src/run/run-detail-render.js +26 -1
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +229 -189
- package/dist/src/run/run-stage.js +112 -43
- package/dist/src/run/run-verdict.js +113 -12
- package/dist/src/run/stage-backlog-checks.js +2 -2
- 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-pi.js +72 -49
- package/dist/src/runner/runner-run.js +17 -23
- package/dist/src/scheduling/claims.js +1 -1
- package/dist/src/status/status-data.js +140 -87
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +49 -0
- package/dist/src/status/status-roles.js +8 -2
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/ui/alert-feeds.js +320 -0
- package/dist/src/ui/badges.js +1 -1
- package/dist/src/ui/gui/gui-client-boot.js +59 -53
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +17 -1
- 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 +103 -4
- package/dist/src/ui/gui/gui-client-report.js +70 -10
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -3
- package/dist/src/ui/gui/gui-client-team.js +2 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- 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 +24 -14
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +130 -79
- package/dist/src/ui/team-alert-backlog-feed.js +181 -0
- package/dist/src/ui/team-alert-backlog.js +267 -0
- package/dist/src/ui/team-alert-merge.js +183 -0
- package/dist/src/ui/team-alert-shared.js +79 -0
- package/dist/src/ui/team-alert-status.js +321 -0
- package/dist/src/ui/team-alert-tail.js +251 -0
- package/dist/src/ui/team-alerts.js +15 -972
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-budget.js +9 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -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 +2 -1
- package/dist/src/ui/tui/tui-editor-prompt.js +2 -1
- package/dist/src/ui/tui/tui-editor-quiet.js +2 -1
- package/dist/src/ui/tui/tui-editor-release.js +77 -0
- package/dist/src/ui/tui/tui-editor-role-file.js +75 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +2 -1
- 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 +5 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +43 -3
- package/dist/src/ui/tui/tui-pane-history.js +299 -263
- package/dist/src/ui/tui/tui-pane-session.js +3 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +12 -8
- package/dist/src/ui/tui/tui-panes.js +209 -145
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/refusal.js +2 -1
- package/package.json +1 -1
- /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/dist/src/merge/{merge-post-review.js → merge-post-review-drain.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.1**: 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
|
|
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,10 @@ 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]);
|
|
@@ -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
18
|
import { secondsUntil } from "../../text/datetime.js";
|
|
19
19
|
import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
|
|
20
|
+
import { SERVER_TRANSIENT_STATUS } 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,9 +30,6 @@ 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
35
|
function str(value) {
|
|
@@ -94,7 +92,7 @@ function classifyResult(result) {
|
|
|
94
92
|
const status = nonNegativeNumber(result.api_error_status, undefined);
|
|
95
93
|
if (status === 429)
|
|
96
94
|
return { kind: "transientRateLimit" };
|
|
97
|
-
if (status !== undefined &&
|
|
95
|
+
if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status))
|
|
98
96
|
return { kind: "transientBackend", backendKind: "server" };
|
|
99
97
|
// The context-overflow text wins over a configuration status: claude reports an oversized
|
|
100
98
|
// prompt as HTTP 400 ("prompt is too long"), whose status alone would read as configError.
|
|
@@ -170,6 +168,167 @@ export function claudeTranslator(opts = {}) {
|
|
|
170
168
|
seen.add(kind);
|
|
171
169
|
warnings.add(kind);
|
|
172
170
|
};
|
|
171
|
+
const handleSystem = (event) => {
|
|
172
|
+
const subtype = str(event.subtype);
|
|
173
|
+
if (subtype === "init") {
|
|
174
|
+
const out = [];
|
|
175
|
+
const cwd = str(event.cwd);
|
|
176
|
+
const id = str(event.session_id);
|
|
177
|
+
out.push(JSON.stringify({ type: "session", ...(cwd !== undefined ? { cwd } : {}), ...(id !== undefined ? { id } : {}) }));
|
|
178
|
+
out.push(JSON.stringify({ type: "agent_start" }));
|
|
179
|
+
const billing = billingOf(str(event.apiKeySource), opts.thirdPartyProvider === true);
|
|
180
|
+
if (billing)
|
|
181
|
+
out.push(JSON.stringify({ type: "billing", billing }));
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
if (subtype === "api_retry") {
|
|
185
|
+
const attempt = nonNegativeNumber(event.attempt, undefined);
|
|
186
|
+
const maxAttempts = nonNegativeNumber(event.max_retries, undefined);
|
|
187
|
+
const status = nonNegativeNumber(event.error_status, undefined);
|
|
188
|
+
const error = str(event.error) ?? "";
|
|
189
|
+
const errorMessage = joinParts([status !== undefined ? `status ${status}` : "", error], ": ");
|
|
190
|
+
return [
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
type: "auto_retry_start",
|
|
193
|
+
...(attempt !== undefined ? { attempt } : {}),
|
|
194
|
+
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
195
|
+
errorMessage: errorMessage || "api retry",
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
if (subtype === "compact_boundary")
|
|
200
|
+
return [JSON.stringify({ type: "compaction_start" })];
|
|
201
|
+
// A thinking-token estimate during extended thinking: known, carries nothing canonical.
|
|
202
|
+
if (subtype === "thinking_tokens")
|
|
203
|
+
return [];
|
|
204
|
+
record(`system:${subtype ?? "unknown"}`);
|
|
205
|
+
return [];
|
|
206
|
+
};
|
|
207
|
+
const handleAssistant = (event) => {
|
|
208
|
+
sawActivity = true;
|
|
209
|
+
const message = objectOrEmpty(event.message);
|
|
210
|
+
const id = str(message.id) ?? `anon-${anonymous++}`;
|
|
211
|
+
// A new id flushes the open message; a continuing one flushes nothing. Either way the
|
|
212
|
+
// fresh or open pending then folds this event's content, usage, and stop reason the
|
|
213
|
+
// same way — that shared fold is why both branches live on one path.
|
|
214
|
+
let out = [];
|
|
215
|
+
if (pending?.id !== id) {
|
|
216
|
+
out = flush();
|
|
217
|
+
pending = { id, content: [], usage: canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined) };
|
|
218
|
+
}
|
|
219
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
220
|
+
for (const raw of blocks) {
|
|
221
|
+
if (!isJsonObject(raw))
|
|
222
|
+
continue;
|
|
223
|
+
const block = canonicalBlock(raw);
|
|
224
|
+
if (!block)
|
|
225
|
+
continue;
|
|
226
|
+
pending.content.push(block);
|
|
227
|
+
if (block.type === "toolCall")
|
|
228
|
+
out.push(startToolCall(block));
|
|
229
|
+
}
|
|
230
|
+
// Usage repeats on every event of the same message; the last read wins and is counted
|
|
231
|
+
// once because the fold emits one message_end per id.
|
|
232
|
+
pending.usage = canonicalUsage(isJsonObject(message.usage) ? message.usage : undefined);
|
|
233
|
+
const stopReason = str(message.stop_reason);
|
|
234
|
+
if (stopReason !== undefined)
|
|
235
|
+
pending.stopReason = stopReason;
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
const handleUser = (event) => {
|
|
239
|
+
const out = flush();
|
|
240
|
+
const message = objectOrEmpty(event.message);
|
|
241
|
+
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
242
|
+
for (const raw of blocks) {
|
|
243
|
+
if (!isJsonObject(raw) || raw.type !== "tool_result")
|
|
244
|
+
continue;
|
|
245
|
+
out.push(JSON.stringify({
|
|
246
|
+
type: "tool_execution_end",
|
|
247
|
+
toolCallId: str(raw.tool_use_id) ?? "",
|
|
248
|
+
isError: raw.is_error === true,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
};
|
|
253
|
+
const handleRateLimitEvent = (event) => {
|
|
254
|
+
const info = objectOrEmpty(event.rate_limit_info);
|
|
255
|
+
const rateLimitType = str(info.rateLimitType);
|
|
256
|
+
const status = str(info.status);
|
|
257
|
+
// claude 2.1.295 reports every window's utilization and reset under `unifiedWindows`,
|
|
258
|
+
// keyed by window type, and only the binding window's status at the top level.
|
|
259
|
+
const windows = [];
|
|
260
|
+
for (const [type, raw] of Object.entries(objectOrEmpty(info.unifiedWindows))) {
|
|
261
|
+
if (type === rateLimitType || !isJsonObject(raw))
|
|
262
|
+
continue;
|
|
263
|
+
const reading = { type: "rate_limit", rateLimitType: type };
|
|
264
|
+
const windowResets = nonNegativeNumber(raw.resetsAt, undefined);
|
|
265
|
+
if (windowResets !== undefined)
|
|
266
|
+
reading.resetsAt = windowResets;
|
|
267
|
+
const windowUtil = nonNegativeNumber(raw.utilization, undefined);
|
|
268
|
+
if (windowUtil !== undefined)
|
|
269
|
+
reading.utilization = windowUtil;
|
|
270
|
+
windows.push(JSON.stringify(reading));
|
|
271
|
+
}
|
|
272
|
+
const own = rateLimitType === undefined ? undefined : objectOrEmpty(objectOrEmpty(info.unifiedWindows)[rateLimitType]);
|
|
273
|
+
const resetsAt = nonNegativeNumber(info.resetsAt, undefined) ?? nonNegativeNumber(own?.resetsAt, undefined);
|
|
274
|
+
const out = { type: "rate_limit" };
|
|
275
|
+
if (rateLimitType !== undefined)
|
|
276
|
+
out.rateLimitType = rateLimitType;
|
|
277
|
+
if (status !== undefined)
|
|
278
|
+
out.status = status;
|
|
279
|
+
if (resetsAt !== undefined)
|
|
280
|
+
out.resetsAt = resetsAt;
|
|
281
|
+
const utilization = nonNegativeNumber(info.utilization, undefined) ?? nonNegativeNumber(own?.utilization, undefined);
|
|
282
|
+
if (utilization !== undefined)
|
|
283
|
+
out.utilization = utilization;
|
|
284
|
+
const overageStatus = str(info.overageStatus);
|
|
285
|
+
if (overageStatus !== undefined)
|
|
286
|
+
out.overageStatus = overageStatus;
|
|
287
|
+
if (info.isUsingOverage === true)
|
|
288
|
+
out.isUsingOverage = true;
|
|
289
|
+
if (status === "rejected" && resetsAt !== undefined) {
|
|
290
|
+
// resetsAt is a unix-seconds stamp; secondsUntil folds it (×1000) to whole seconds.
|
|
291
|
+
out.retryAfterSeconds = secondsUntil(resetsAt * 1000, Date.now());
|
|
292
|
+
}
|
|
293
|
+
return [JSON.stringify(out), ...windows];
|
|
294
|
+
};
|
|
295
|
+
const handleResult = (event) => {
|
|
296
|
+
sawActivity = true;
|
|
297
|
+
const out = flush();
|
|
298
|
+
// The assistant events' usage is the message-start snapshot, so the result's total
|
|
299
|
+
// is the run's real output-token count (thinking included).
|
|
300
|
+
const outputTokens = nonNegativeNumber(objectOrEmpty(event.usage).output_tokens, undefined);
|
|
301
|
+
if (outputTokens !== undefined)
|
|
302
|
+
out.push(JSON.stringify({ type: "run_usage", outputTokens }));
|
|
303
|
+
const total = nonNegativeNumber(event.total_cost_usd, undefined);
|
|
304
|
+
if (total !== undefined) {
|
|
305
|
+
sawRunCost = true;
|
|
306
|
+
out.push(JSON.stringify({ type: "run_cost", total }));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
warnings.add("cost_unknown");
|
|
310
|
+
}
|
|
311
|
+
if (event.is_error === true) {
|
|
312
|
+
const { kind, backendKind } = classifyResult(event);
|
|
313
|
+
const message = {
|
|
314
|
+
role: "assistant",
|
|
315
|
+
content: [],
|
|
316
|
+
stopReason: "error",
|
|
317
|
+
};
|
|
318
|
+
const errorMessage = str(event.result) ?? str(event.error);
|
|
319
|
+
if (errorMessage !== undefined)
|
|
320
|
+
message.errorMessage = errorMessage;
|
|
321
|
+
// tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
|
|
322
|
+
// reads event.tumwaterKind and trusts it before its text regexes.
|
|
323
|
+
out.push(JSON.stringify({
|
|
324
|
+
type: "message_end",
|
|
325
|
+
message,
|
|
326
|
+
...(kind !== undefined ? { tumwaterKind: kind } : {}),
|
|
327
|
+
...(backendKind !== undefined ? { backendKind } : {}),
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
};
|
|
173
332
|
const translate = (line) => {
|
|
174
333
|
const parsed = parseJsonObject(line);
|
|
175
334
|
if (!parsed)
|
|
@@ -177,167 +336,11 @@ export function claudeTranslator(opts = {}) {
|
|
|
177
336
|
const event = parsed;
|
|
178
337
|
const type = str(event.type);
|
|
179
338
|
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
|
-
}
|
|
339
|
+
case "system": return handleSystem(event);
|
|
340
|
+
case "assistant": return handleAssistant(event);
|
|
341
|
+
case "user": return handleUser(event);
|
|
342
|
+
case "rate_limit_event": return handleRateLimitEvent(event);
|
|
343
|
+
case "result": return handleResult(event);
|
|
341
344
|
case "tool_progress":
|
|
342
345
|
// A 30 s heartbeat of a running tool call: known, and the open call already tracks
|
|
343
346
|
// 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.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { walkFiles } from "../../files/files.js";
|
|
15
16
|
import { isJsonObject, nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
|
|
16
17
|
import { windowTypeName } from "./codex-stream.js";
|
|
17
18
|
function str(value) {
|
|
@@ -155,25 +156,13 @@ export function findRollout(home, id) {
|
|
|
155
156
|
if (!fs.existsSync(sessionsRoot))
|
|
156
157
|
return null;
|
|
157
158
|
const suffix = `-${id}.jsonl`;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
for (const entry of entries) {
|
|
169
|
-
const full = path.join(dir, entry.name);
|
|
170
|
-
if (entry.isDirectory()) {
|
|
171
|
-
stack.push(full);
|
|
172
|
-
}
|
|
173
|
-
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix)) {
|
|
174
|
-
return full;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
159
|
+
let found = null;
|
|
160
|
+
// The walk-and-swallow recursion is files.ts's walkFiles (an unreadable dir is no match);
|
|
161
|
+
// the id match alone decides, so only the one id's rollout can win. The sessions tree is one
|
|
162
|
+
// directory per active day, so walking it whole to the first match stays small.
|
|
163
|
+
walkFiles(sessionsRoot, (file, entry) => {
|
|
164
|
+
if (found === null && entry.name.startsWith("rollout-") && entry.name.endsWith(suffix))
|
|
165
|
+
found = file;
|
|
166
|
+
});
|
|
167
|
+
return found;
|
|
179
168
|
}
|