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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** The build-and-spawn stage of the agent runner (plans/agent-backends.md "The seam"): the
|
|
2
|
+
* part of a run before the stream pipeline — the `tumwater_run` raw-log marker, the stream
|
|
3
|
+
* parser, the run marker, backend-bin resolution, the notes shim, the run spec, the backend's
|
|
4
|
+
* optional `prepare`, the session-ledger record, the native-to-canonical translator, the two
|
|
5
|
+
* incremental decoders, and the child spawn. It returns the `SpawnedRun` bundle the coordinator
|
|
6
|
+
* (src/agent/run-agent.ts) binds its watchdogs and settlement wiring to, and passes to
|
|
7
|
+
* createAgentStream (src/agent/run-stream.ts).
|
|
8
|
+
* Part 1/2 of the runAgent split: the streaming pipeline (part 2/2) lives in run-stream.ts.
|
|
9
|
+
* This module imports only run-agent's type surface, so
|
|
10
|
+
* there is no runtime import cycle. */
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { StringDecoder } from "node:string_decoder";
|
|
14
|
+
import { ensureParentDir, rotateIfLarge } from "../files/files.js";
|
|
15
|
+
import { terminateChild } from "../process/process.js";
|
|
16
|
+
import { makeRunMarker } from "../process/run-marker.js";
|
|
17
|
+
import { PiStreamParser } from "../pi/pi-stream.js";
|
|
18
|
+
import { recordSession, sessionRecordFor } from "./session-ledger.js";
|
|
19
|
+
import { errorMessage } from "../text/text.js";
|
|
20
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
21
|
+
import { seamImageInput } from "./image-input.js";
|
|
22
|
+
import { ensureNotesShim } from "./notes-shim.js";
|
|
23
|
+
import { nearestTumwaterRoot } from "../paths.js";
|
|
24
|
+
import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
25
|
+
export function openAppendLog(file, maxBytes) {
|
|
26
|
+
ensureParentDir(file);
|
|
27
|
+
rotateIfLarge(file, maxBytes);
|
|
28
|
+
const log = { stream: fs.createWriteStream(file, { flags: "a" }), broken: false };
|
|
29
|
+
log.stream.on("error", () => {
|
|
30
|
+
log.broken = true;
|
|
31
|
+
});
|
|
32
|
+
return log;
|
|
33
|
+
}
|
|
34
|
+
/** Invoke the backend and spawn the child from the pre-spawn pieces, returning the bundle. */
|
|
35
|
+
function buildAndSpawn(backend, opts, pre) {
|
|
36
|
+
const { runSpec, parser, runMarker, resolved, blind, runTmp } = pre;
|
|
37
|
+
const invocation = backend.invocation(runSpec);
|
|
38
|
+
// The id the pre-spawn ledger record carries: a backend that mints one ahead of the run
|
|
39
|
+
// (claude, a codex resume), else the session name (pi, a fresh codex run — its id is born in
|
|
40
|
+
// the stream). The stream-mint pass later compares against it so a codex run appends its
|
|
41
|
+
// streamed thread id exactly once, and only when it differs from this.
|
|
42
|
+
const preSpawnSessionId = invocation.sessionId ?? opts.sessionName;
|
|
43
|
+
// Record the run in the session ledger before its spawn, so a later resume can pick the newest
|
|
44
|
+
// session of its own kind instead of whatever file is newest in the dir. The id is the backend's
|
|
45
|
+
// own when it mints one ahead of the run (claude's UUID, or the resumed record's id); pi mints
|
|
46
|
+
// none and keeps the session name.
|
|
47
|
+
recordSession(opts.sessionDir, sessionRecordFor(backend.id, preSpawnSessionId, opts));
|
|
48
|
+
const translator = backend.translator();
|
|
49
|
+
// Decode stdout incrementally instead of per chunk: a raw Buffer.toString("utf8") replaces any
|
|
50
|
+
// multi-byte character whose bytes straddle two 'data' events with U+FFFD, corrupting that
|
|
51
|
+
// line's text (commit subjects, summaries, transcripts). StringDecoder holds back the incomplete
|
|
52
|
+
// trailing bytes until the next chunk completes them.
|
|
53
|
+
const decoder = new StringDecoder("utf8");
|
|
54
|
+
// stderr gets its own incremental decoder for the same reason: the failure message and the
|
|
55
|
+
// stderr-keyed matchers (crash, stream-severed, config) read this buffer, so a character whose
|
|
56
|
+
// bytes straddle two 'data' events must not become U+FFFD.
|
|
57
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
58
|
+
const child = spawn(resolved.bin, invocation.args, {
|
|
59
|
+
cwd: opts.cwd,
|
|
60
|
+
// A backend that feeds the prompt on stdin (every registered one) gets a pipe the runner
|
|
61
|
+
// writes and ends below; one that returns no stdin keeps stdio[0] "ignore".
|
|
62
|
+
stdio: [invocation.stdin !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
63
|
+
// pi sets its process title at startup, and so does every npm its tool calls run: on macOS
|
|
64
|
+
// each would register with LaunchServices and leak a launchservicesd port. Detached so pi
|
|
65
|
+
// leads its own process group: terminateChild signals the group, so a run's tool-call
|
|
66
|
+
// grandchildren — killed or finished — die with it instead of leaking to launchd.
|
|
67
|
+
detached: true,
|
|
68
|
+
// The run's mark, inherited by every tool call and its descendants: pi 0.87.1's bash tool
|
|
69
|
+
// spawns each command detached in its OWN group, so the group sweep below cannot reach what
|
|
70
|
+
// a tool call backgrounds (BUGS.md 2026-09-30) — the mark is the only attribution that
|
|
71
|
+
// follows a reparented orphan. Appended to any inherited mark so a nested harness under test
|
|
72
|
+
// keeps the outer run's.
|
|
73
|
+
env: { ...invocation.env, TMPDIR: runTmp },
|
|
74
|
+
});
|
|
75
|
+
if (invocation.stdin !== undefined) {
|
|
76
|
+
// A backend that dies before draining its stdin — an immediately-failing startup such as an
|
|
77
|
+
// unknown flag, an unresumable session, or a not-logged-in exit — leaves this write nothing
|
|
78
|
+
// to feed. Once the prompt exceeds the OS pipe buffer the write then fails EPIPE, and an
|
|
79
|
+
// unhandled 'error' event on the stdin stream is thrown from the event loop and takes the
|
|
80
|
+
// whole process (the whole team) down. The child's own exit status and stderr already report
|
|
81
|
+
// why the run died, so swallowing the failed write adds no silence the caller needs to break;
|
|
82
|
+
// git.ts's patchId carries the same guard for a patch-id that dies before reading its input.
|
|
83
|
+
child.stdin?.on("error", () => { });
|
|
84
|
+
child.stdin?.end(invocation.stdin);
|
|
85
|
+
}
|
|
86
|
+
// Armed now that the child exists, so the parser's parse-time onBlindRead (fires during feed,
|
|
87
|
+
// always after spawn) can reach it.
|
|
88
|
+
blind.terminate = () => terminateChild(child);
|
|
89
|
+
return {
|
|
90
|
+
child,
|
|
91
|
+
resolved,
|
|
92
|
+
runSpec,
|
|
93
|
+
invocation,
|
|
94
|
+
parser,
|
|
95
|
+
translator,
|
|
96
|
+
decoder,
|
|
97
|
+
stderrDecoder,
|
|
98
|
+
runMarker,
|
|
99
|
+
preSpawnSessionId,
|
|
100
|
+
blindRead: () => blind.read,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Build and spawn one non-interactive agent run, from the marker write through the child spawn,
|
|
104
|
+
* and return the bundle the run's stream pipeline, watchdogs and settlement wiring consume. The
|
|
105
|
+
* caller's setup (opened logs, the run's private TMPDIR) is passed in; a synchronous throw here
|
|
106
|
+
* propagates to the caller, which rejects the run's promise. The backend's optional `prepare` is
|
|
107
|
+
* awaited here and warned about (never thrown away) exactly as the coordinator once did.
|
|
108
|
+
*
|
|
109
|
+
* The return type is a union on purpose: a backend without a `prepare` step (pi, claude,
|
|
110
|
+
* opencode) builds and spawns synchronously and returns the bundle DIRECTLY, so the coordinator
|
|
111
|
+
* does not `await` it and its watchdogs (a setInterval registration) start in the same
|
|
112
|
+
* synchronous step — the ordering the logical-clock watchdog tests pin, which advance the fake
|
|
113
|
+
* clock immediately after the run promise is created. A backend WITH a `prepare` (codex)
|
|
114
|
+
* resolves its trust first and returns a promise that resolves to the bundle after the await. */
|
|
115
|
+
export function spawnPiRun(backend, logs, runTmp, opts) {
|
|
116
|
+
const rawLog = logs.raw;
|
|
117
|
+
// The first blind image read an authoring run made (plans/image-input.md "Detecting a blind
|
|
118
|
+
// read"): the path, plus an indirection to the child so the parser's parse-time callback
|
|
119
|
+
// (which fires during feed, after spawn) can kill it. Unset for a gate run, which only records
|
|
120
|
+
// the read. Declared before the parser so its onBlindRead closure (which assigns them at parse
|
|
121
|
+
// time, after spawn) can reference them.
|
|
122
|
+
const blind = {};
|
|
123
|
+
// The marker precedes this run's first event in file order — written to the same stream
|
|
124
|
+
// stdout lines flow through, before spawn, so ordering is exact by construction; on a failed
|
|
125
|
+
// spawn finish() still ends the stream and flushes it. Exactly one per run: the kind, agent
|
|
126
|
+
// and start time are always there, the label only when the caller set one (`history
|
|
127
|
+
// <role> <n> --transcript` selects a run's segments by this ts).
|
|
128
|
+
{
|
|
129
|
+
const marker = {
|
|
130
|
+
type: "tumwater_run",
|
|
131
|
+
kind: opts.kind,
|
|
132
|
+
agent: backend.id,
|
|
133
|
+
ts: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
if (opts.label)
|
|
136
|
+
marker.label = opts.label;
|
|
137
|
+
rawLog.stream.write(JSON.stringify(marker) + "\n");
|
|
138
|
+
}
|
|
139
|
+
const parser = new PiStreamParser(opts.onToolCallStart, {
|
|
140
|
+
// The seam's effective image input (src/agent/image-input.ts): a text-only seam makes every
|
|
141
|
+
// image read blind even when pi adds no note — the parser checks against its own note rule.
|
|
142
|
+
imageInput: seamImageInput(opts.config, readPiProviders(piModelsPath())),
|
|
143
|
+
onBlindRead: (path) => {
|
|
144
|
+
// Kill an authoring run at once, before the agent goes on working from a picture it never
|
|
145
|
+
// saw (terminateChild is what the watchdogs use). Gate runs (reviewer, resolver) only
|
|
146
|
+
// record the read and keep going.
|
|
147
|
+
if (opts.kind === "author") {
|
|
148
|
+
blind.read = path;
|
|
149
|
+
blind.terminate?.();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
// The run's cross-group attribution mark, minted before the spawn so the environment can carry
|
|
154
|
+
// it and the exit sweep can name it.
|
|
155
|
+
const runMarker = makeRunMarker();
|
|
156
|
+
const resolved = backend.resolveBin(opts.config);
|
|
157
|
+
// Install the notes shim under the team root's `.tumwater/bin` so every backend's run finds
|
|
158
|
+
// `tumwater-notes` first on PATH (notes-shim.ts). A failure to write it logs one warning and
|
|
159
|
+
// the run goes on without it — notes would then be unwritable by the command, but the run
|
|
160
|
+
// itself is not blocked.
|
|
161
|
+
let binDir;
|
|
162
|
+
try {
|
|
163
|
+
// Install the shim only under a real team root — the nearest ancestor holding a `.tumwater`.
|
|
164
|
+
// A session dir with no such ancestor (a lab script, a smoke run, a prompt-lab replay) must
|
|
165
|
+
// never mint one: nearestTumwaterRoot is undefined then, and the run goes on without the
|
|
166
|
+
// shim, exactly as if the install had failed — writing `<fallback>/.tumwater` from a run
|
|
167
|
+
// created a phantom harness root in a shared directory and broke every other suite on the
|
|
168
|
+
// host (BUGS.md 2026-10-18).
|
|
169
|
+
const root = nearestTumwaterRoot(opts.sessionDir);
|
|
170
|
+
if (root !== undefined)
|
|
171
|
+
binDir = ensureNotesShim(root);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
opts.onAgentWarning?.("could not install the tumwater-notes shim; this run will not find tumwater-notes on PATH");
|
|
175
|
+
}
|
|
176
|
+
const runSpec = {
|
|
177
|
+
cwd: opts.cwd,
|
|
178
|
+
prompt: opts.prompt,
|
|
179
|
+
config: opts.config,
|
|
180
|
+
sessionDir: opts.sessionDir,
|
|
181
|
+
sessionName: opts.sessionName,
|
|
182
|
+
continueSession: opts.continueSession,
|
|
183
|
+
kind: opts.kind,
|
|
184
|
+
agentBin: resolved.bin,
|
|
185
|
+
runMarker,
|
|
186
|
+
notesPath: opts.notesPath,
|
|
187
|
+
...(binDir !== undefined ? { binDir } : {}),
|
|
188
|
+
...(opts.maxBudgetUsd !== undefined ? { maxBudgetUsd: opts.maxBudgetUsd } : {}),
|
|
189
|
+
};
|
|
190
|
+
// A backend's asynchronous pre-run work (the codex write guard resolving its trust by spawning
|
|
191
|
+
// `app-server`, plans/codex-write-guard.md "Trust (parts 4–5)"): awaited just before the
|
|
192
|
+
// synchronous invocation, so trust is settled before invocation reads it. It never throws a run
|
|
193
|
+
// away — a throw is a warm-up failure, warned about on the run's own root (skipped with no root)
|
|
194
|
+
// and the run goes on; a codex trust lookup failure already records `off` and warns from prepare
|
|
195
|
+
// itself, so this catch is the guard for a backend whose prepare throws unexpectedly. The call
|
|
196
|
+
// itself is also guarded: a synchronous `prepare` throw would otherwise escape and (on the
|
|
197
|
+
// asynchronous path) leave the run's promise pending, so it is warned-and-continued like an
|
|
198
|
+
// async rejection.
|
|
199
|
+
const warnRoot = nearestTumwaterRoot(opts.sessionDir);
|
|
200
|
+
let prepared;
|
|
201
|
+
try {
|
|
202
|
+
prepared = backend.prepare?.(runSpec);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (warnRoot !== undefined) {
|
|
206
|
+
warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (prepared === undefined) {
|
|
210
|
+
// No `prepare` step: build and spawn synchronously and hand the bundle straight back, so
|
|
211
|
+
// the coordinator's watchdog start lands in the same synchronous step (see the doc comment).
|
|
212
|
+
return buildAndSpawn(backend, opts, { runSpec, parser, runMarker, resolved, blind, runTmp });
|
|
213
|
+
}
|
|
214
|
+
// Only a backend with a `prepare` step (codex) awaits it, then builds and spawns.
|
|
215
|
+
return (async () => {
|
|
216
|
+
try {
|
|
217
|
+
await prepared;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (warnRoot !== undefined) {
|
|
221
|
+
warnEventBestEffort(warnRoot, TUMWATER_SOURCE, `agent prepare failed, run continues: ${errorMessage(error)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return buildAndSpawn(backend, opts, { runSpec, parser, runMarker, resolved, blind, runTmp });
|
|
225
|
+
})();
|
|
226
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** The streaming pipeline of the backend agent runner (plans/agent-backends.md "The seam"):
|
|
2
|
+
* everything the run does live with the child's stdout/stderr after spawn — the native-to-
|
|
3
|
+
* canonical stdout line splitting with its over-long-line discard, the stderr window and its
|
|
4
|
+
* fallback-clone warning matcher, the stream-minted session's second ledger record, and the
|
|
5
|
+
* settle/flush that resolves the run's promise. It is the part of the runAgent split (part
|
|
6
|
+
* 2/2) that the coordinator (src/agent/run-agent.ts) delegates to behind the `AgentStream`
|
|
7
|
+
* interface: run-agent wires the child's `data` events to `feedStdout`/`feedStderr`, binds the
|
|
8
|
+
* settlement state's stream getters, and keeps the watchdogs, abort wiring and the
|
|
9
|
+
* `error`/`exit`/`close` listeners itself. This module imports only run-agent's, run-spawn's
|
|
10
|
+
* and run-settle's type surfaces, so there is no runtime import cycle. */
|
|
11
|
+
import { removeTree } from "../files/files.js";
|
|
12
|
+
import { recordSession, sessionRecordFor } from "./session-ledger.js";
|
|
13
|
+
/** How many trailing stderr characters are carried into the next chunk's warning match, so a
|
|
14
|
+
* warning split across two `data` events is still seen. The warnings are short lines, so a few
|
|
15
|
+
* hundred characters always cover a straddle. */
|
|
16
|
+
const STDERR_MATCH_TAIL = 512;
|
|
17
|
+
/** The largest native stdout line the runner will buffer. A complete native event is one JSON
|
|
18
|
+
* object; a line this long is already at the default whole-log cap (`logMaxBytes`, 16 MiB), so
|
|
19
|
+
* continuing to buffer it can only be a malformed stream. Past it `feedCanonical` drops the
|
|
20
|
+
* buffered line and skips input to the next newline, so a backend that streams bytes with no
|
|
21
|
+
* newline — each byte resetting the quiet watchdog — cannot grow the buffer until the run
|
|
22
|
+
* timeout (4 h by default) exhausts the harness's memory. */
|
|
23
|
+
export const MAX_NATIVE_LINE_CHARS = 16 * 1024 * 1024;
|
|
24
|
+
/** Build the streaming pipeline for one spawned run. The coordinator passes the bundle, its
|
|
25
|
+
* watchdogs, the abort wiring, the two logs the flush-at-settle writes, the run's TMPDIR it
|
|
26
|
+
* removes at settle, and the promise's `resolve` — everything the moved closures captured
|
|
27
|
+
* before the refactor — and this call returns the single `AgentStream` the coordinator binds
|
|
28
|
+
* the child's `data` events and the settlement state to. */
|
|
29
|
+
export function createAgentStream(input) {
|
|
30
|
+
const { opts, backend, spawned, wd, onAbort, rawLog, nativeLog, runTmp, resolve } = input;
|
|
31
|
+
const { parser, translator, decoder, stderrDecoder, preSpawnSessionId } = spawned;
|
|
32
|
+
// The stream's mutable state (its own per-run locals, shared by exactly the closures below):
|
|
33
|
+
// the stderr window and its warning matcher tail, the settled guard, and the translator's
|
|
34
|
+
// native-line buffer.
|
|
35
|
+
let stderr = "";
|
|
36
|
+
// The fallback-clone warning arrives at the START of a run, while `stderr` keeps only the
|
|
37
|
+
// last 64 KiB; match each chunk (with a short carried tail for a straddle) so a chatty run
|
|
38
|
+
// that pushes the warning out of that window still surfaces the mispriced id.
|
|
39
|
+
let fallbackClone;
|
|
40
|
+
let stderrTail = "";
|
|
41
|
+
let settled = false;
|
|
42
|
+
// The translator's native line buffer: feed the decoder's text through it and only complete
|
|
43
|
+
// lines reach the translator, whose canonical output feeds the parser (and the raw log).
|
|
44
|
+
// `nativeOverflow` marks a buffered line that passed MAX_NATIVE_LINE_CHARS without a newline:
|
|
45
|
+
// its remainder is skipped to the next newline so it can never be mistaken for an event.
|
|
46
|
+
let nativeTail = "";
|
|
47
|
+
let nativeOverflow = false;
|
|
48
|
+
const onLine = (line) => rawLog.stream.write(line + "\n");
|
|
49
|
+
// The translator's session id, once the stream has named it (codex's `thread.started`).
|
|
50
|
+
// The pre-spawn record for a fresh codex run carries the session name; the id it actually
|
|
51
|
+
// ran with is born in the stream, so the first time this returns one the pre-spawn record
|
|
52
|
+
// lacked, append a second ledger record carrying it (same kind, name, cwd and model) — the
|
|
53
|
+
// record a later `exec resume` resumes (design §4 "Sessions").
|
|
54
|
+
let streamMintedSession = false;
|
|
55
|
+
const emitNativeLine = (line) => {
|
|
56
|
+
if (!nativeLog.broken)
|
|
57
|
+
nativeLog.stream.write(line + "\n");
|
|
58
|
+
for (const canonical of translator.translate(line))
|
|
59
|
+
parser.feed(canonical + "\n", onLine);
|
|
60
|
+
const sid = translator.sessionId?.();
|
|
61
|
+
if (sid !== undefined && sid !== preSpawnSessionId && !streamMintedSession) {
|
|
62
|
+
streamMintedSession = true;
|
|
63
|
+
recordSession(opts.sessionDir, sessionRecordFor(backend.id, sid, opts));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Split native stdout into complete lines, translate each, and feed the canonical lines to
|
|
67
|
+
// the parser (which also hands them to onLine for the raw log). A trailing partial native
|
|
68
|
+
// line stays buffered exactly as the parser's own buffer once held it. On the final flush
|
|
69
|
+
// (`final`), a remaining tail is emitted as one line: a backend may close after flushing a
|
|
70
|
+
// complete JSON event but not its newline terminator, and dropping that tail loses the
|
|
71
|
+
// run's authoritative result (cost, final text). A genuinely truncated fragment parses as
|
|
72
|
+
// no data (parseJsonObject → []), so this can only recover the last event.
|
|
73
|
+
const feedCanonical = (text, final = false) => {
|
|
74
|
+
// A buffered overflow: discard this line's remainder up to (and including) its newline, so
|
|
75
|
+
// the stream resynchronizes on the next real event line instead of emitting a truncated one.
|
|
76
|
+
if (nativeOverflow) {
|
|
77
|
+
const nl = text.indexOf("\n");
|
|
78
|
+
if (nl < 0)
|
|
79
|
+
return;
|
|
80
|
+
nativeOverflow = false;
|
|
81
|
+
text = text.slice(nl + 1);
|
|
82
|
+
}
|
|
83
|
+
nativeTail += text;
|
|
84
|
+
let start = 0;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const nl = nativeTail.indexOf("\n", start);
|
|
87
|
+
if (nl < 0)
|
|
88
|
+
break;
|
|
89
|
+
emitNativeLine(nativeTail.slice(start, nl));
|
|
90
|
+
start = nl + 1;
|
|
91
|
+
}
|
|
92
|
+
if (start > 0)
|
|
93
|
+
nativeTail = nativeTail.slice(start);
|
|
94
|
+
if (nativeTail.length > MAX_NATIVE_LINE_CHARS) {
|
|
95
|
+
// No newline within the cap: drop the line and warn. On the final flush there is no
|
|
96
|
+
// remainder to skip, so only the buffer is cleared.
|
|
97
|
+
nativeTail = "";
|
|
98
|
+
if (!final) {
|
|
99
|
+
nativeOverflow = true;
|
|
100
|
+
opts.onAgentWarning?.(`native output line exceeded ${MAX_NATIVE_LINE_CHARS / (1024 * 1024)} MiB without a newline and was discarded`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (final && nativeTail !== "") {
|
|
104
|
+
emitNativeLine(nativeTail);
|
|
105
|
+
nativeTail = "";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const feedStdout = (chunk) => {
|
|
109
|
+
wd.noteByte();
|
|
110
|
+
feedCanonical(decoder.write(chunk));
|
|
111
|
+
};
|
|
112
|
+
const feedStderr = (chunk) => {
|
|
113
|
+
// stderr is rare and meaningful (crash traces, warnings): treat it as progress.
|
|
114
|
+
wd.noteStderr();
|
|
115
|
+
const text = stderrDecoder.write(chunk);
|
|
116
|
+
fallbackClone ??= backend.matchStderrWarning?.(text, stderrTail);
|
|
117
|
+
stderrTail = (stderrTail + text).slice(-STDERR_MATCH_TAIL);
|
|
118
|
+
stderr += text;
|
|
119
|
+
if (stderr.length > 64 * 1024)
|
|
120
|
+
stderr = stderr.slice(-64 * 1024);
|
|
121
|
+
};
|
|
122
|
+
const finish = (result) => {
|
|
123
|
+
if (settled)
|
|
124
|
+
return;
|
|
125
|
+
settled = true;
|
|
126
|
+
// A blind image read (plans/image-input.md "Detecting a blind read"): surface it to the
|
|
127
|
+
// caller once, at settle, with the action its kind earned — the authoring run was killed
|
|
128
|
+
// ("fail"), a gate run only recorded it ("logged"). The caller logs the image_blind event
|
|
129
|
+
// with its own role/run context.
|
|
130
|
+
if (parser.imageBlind) {
|
|
131
|
+
opts.onBlindRead?.(parser.imageBlind.path, opts.kind === "author" ? "fail" : "logged");
|
|
132
|
+
}
|
|
133
|
+
// Best-effort: an unremovable scratch dir (read-only subdir, a straggler still writing) must
|
|
134
|
+
// not throw out of the close handler before the run's result resolves.
|
|
135
|
+
try {
|
|
136
|
+
removeTree(runTmp);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
/* left for the OS temp sweep */
|
|
140
|
+
}
|
|
141
|
+
wd.stop();
|
|
142
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
143
|
+
// Surface the translator's per-run warnings once, after all native lines were fed.
|
|
144
|
+
for (const warning of translator.warnings?.() ?? [])
|
|
145
|
+
opts.onAgentWarning?.(warning);
|
|
146
|
+
// Resolve only once the logs have flushed: writes complete on libuv's threadpool, so
|
|
147
|
+
// resolving before 'finish' can leave the tail of <role>.pi.jsonl (or the native log)
|
|
148
|
+
// unwritten — a run that dies right after runAgent settles (an abort during a restart
|
|
149
|
+
// drain, a supervisor swap) loses its last lines. end()'s callback fires on 'finish';
|
|
150
|
+
// if a stream errors instead it never does, so settle from 'error' too — a broken log
|
|
151
|
+
// degrades to a lost log, never a stuck run.
|
|
152
|
+
let resolved = false;
|
|
153
|
+
const settle = () => {
|
|
154
|
+
if (!resolved) {
|
|
155
|
+
resolved = true;
|
|
156
|
+
resolve(result);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
let pending = 0;
|
|
160
|
+
const flushed = () => {
|
|
161
|
+
pending -= 1;
|
|
162
|
+
if (pending === 0)
|
|
163
|
+
settle();
|
|
164
|
+
};
|
|
165
|
+
for (const log of [rawLog, nativeLog]) {
|
|
166
|
+
log.stream.on("error", settle);
|
|
167
|
+
if (!log.broken) {
|
|
168
|
+
pending += 1;
|
|
169
|
+
log.stream.end(flushed);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (pending === 0)
|
|
173
|
+
settle();
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
feedStdout,
|
|
177
|
+
feedStderr,
|
|
178
|
+
onLine,
|
|
179
|
+
feedCanonical,
|
|
180
|
+
finish,
|
|
181
|
+
settled: () => settled,
|
|
182
|
+
stderr: () => stderr,
|
|
183
|
+
fallbackClone: () => fallbackClone,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -38,6 +38,22 @@ export function recordSession(dir, record) {
|
|
|
38
38
|
// Best effort: a lost ledger line costs resumability, not the run.
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** The one run's ledger record, minus the id the two recording sites mint differently — the
|
|
42
|
+
* pre-spawn id in run-spawn.ts, or the stream-minted session id in run-stream.ts. One place
|
|
43
|
+
* for the record's non-id shape (agent, kind, name, cwd, the optional model and the stamp), so
|
|
44
|
+
* the two appends cannot drift on the model spread or the required fields. `agent` is the
|
|
45
|
+
* owning backend's id; the run options supply the rest. */
|
|
46
|
+
export function sessionRecordFor(agent, id, opts) {
|
|
47
|
+
return {
|
|
48
|
+
agent,
|
|
49
|
+
id,
|
|
50
|
+
kind: opts.kind,
|
|
51
|
+
name: opts.sessionName,
|
|
52
|
+
cwd: opts.cwd,
|
|
53
|
+
...(opts.config.model !== undefined ? { model: opts.config.model } : {}),
|
|
54
|
+
at: Date.now(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
41
57
|
/** True when `dir` holds at least one session file a backend could resume by directory. This is
|
|
42
58
|
* the backend-neutral filesystem check behind pi's `--continue` and the fallback for an
|
|
43
59
|
* unregistered backend. The ledger file shares the dir and is a `.jsonl` too, so it is
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The write guard on a run's file-editing tools: pi's `edit`/`write` (src/pi-extension/
|
|
2
|
-
* write-guard.ts), claude's `Edit`/`Write` (src/agent/claude/write-guard-hook.ts) and
|
|
2
|
+
* write-guard.ts), claude's `Edit`/`Write` (src/agent/claude/claude-write-guard-hook.ts) and
|
|
3
|
+
* OpenCode's
|
|
3
4
|
* `edit` permission (src/agent/opencode/write-guard-plugin/index.ts) refuse a write that would
|
|
4
5
|
* land under the team root but outside the run's own checkout.
|
|
5
6
|
*
|
|
@@ -124,8 +125,9 @@ export function strayWriteRefusal(target, runDir, resolveFrom = runDir) {
|
|
|
124
125
|
+ (instead ?? "Leave it alone and work only inside the worktree.");
|
|
125
126
|
}
|
|
126
127
|
/** The stdin-driven guard-hook driver shared by the claude and codex PreToolUse hooks
|
|
127
|
-
* (src/agent/claude/write-guard-hook.ts, src/agent/codex/write-guard-hook.ts). Each
|
|
128
|
-
* `node write-guard-hook.js`
|
|
128
|
+
* (src/agent/claude/claude-write-guard-hook.ts, src/agent/codex/codex-write-guard-hook.ts). Each
|
|
129
|
+
* hook runs as `node claude-write-guard-hook.js` / `node codex-write-guard-hook.js` with the
|
|
130
|
+
* backend's hook payload JSON on stdin; this reads it in one
|
|
129
131
|
* place, asks `computeRefusal` for a refusal string (null lets the edit/patch run), and prints the
|
|
130
132
|
* `deny` decision only when there is a refusal. Any failure — unreadable stdin, malformed JSON, a
|
|
131
133
|
* throw — reads as "no refusal" (null), so a broken hook cannot stop every edit of a run; the
|