pi-claude-supervisor 0.6.0 → 0.7.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 CHANGED
@@ -6,90 +6,244 @@
6
6
 
7
7
  English · [简体中文](README.cn.md)
8
8
 
9
- A policy-gated [Pi](https://pi.dev) extension for supervising a Claude Code worker.
10
- The MVP keeps Pi in control of lifecycle, state, policy and verification while the
11
- worker remains an explicitly started child process.
12
-
13
- > **Release status:** `v0.5.3` is the released single-Worker recovery baseline. The
14
- > default manual transport is dependency-free process pipes, not PTY. Automatic
15
- > supervision uses Claude JSONL or the Supervisor-owned tmux bridge; adopted tmux
16
- > sessions remain manual-only. Repairable-vs-persistent capabilities,
17
- > cancellable verification, evidence completeness gates, startup preflight and phase
18
- > progress reporting. A real edit-capable Claude Code `2.1.270` repair/reacceptance
19
- > drill passed in an isolated temporary worktree. Real-Claude validation resolves the
20
- > current executable from `PATH` and accepts Claude Code `2.1.270` or newer. The
21
- > confirmed product target is unattended local development; see [the autonomy target](docs/autonomy-target.md).
22
- > Remote push and merge into the main/integration branch remain outside Worker authority
23
- > and must cross an independent boundary.
24
- >
25
- > **Autonomy status:** automatic mode continues local editing, testing, bounded repair,
26
- > acceptance, independent Review and local-commit enforcement without a synchronous human
27
- > callback. Unresolvable work is parked as a non-publishable candidate; optional outbound
28
- > notifications do not approve actions.
29
-
30
- ## Safety boundary
31
-
32
- - The extension never starts a worker automatically.
33
- - Worker commands are launched without a shell.
34
- - Manual workers retain the small inherited environment unless the caller supplies explicit variables. Automatic Claude workers inherit the supervisor environment unchanged except for `CLAUDECODE`, which must be removed so Claude can intentionally launch nested Claude sessions; credentials, Git/package helpers, custom settings and network configuration are not filtered.
35
- - The full Claude Code tool surface is available in automatic mode, including agents, background tasks, plugins and MCP. Automatic mode refuses CLI/settings rules that pre-authorize `Bash`, and adds Claude's safe `default` permission mode when none is supplied, so Bash requests remain visible to the Supervisor; it does not remove the Bash tool. The adapter also adds stream-json transport framing and keeps every Worker descendant inside the Supervisor-owned cleanup boundary. `AskUserQuestion` is converted to ordinary text because no human is synchronously present.
36
- - Local command and permission behavior follows the configured task/runtime policy; known direct remote push/main-integration operations and protected Git metadata remain rejected or parked without requiring a synchronous human response. Nested/custom tools run with the inherited capabilities and are cleaned with the Worker; they are not a second Supervisor permission loop.
37
- - Automatic mode still admits only the bare `claude`/`claude.exe` command name, resolves and pins an operator-owned executable from the supervisor PATH (or `PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE`), and rejects explicit paths or writable/untrusted locations. Manual/custom integrations must provide their own executable and host authority boundary.
38
- - A worker completion is only a transition to `verifying`; it is not evidence of success.
39
- - Verification is an independent host command (default: `git diff --check`).
40
- - Target local development runs unattended after a task starts: the Worker may edit, test, repair and commit locally. Supervisor-managed requests for remote push or merge into `main`/an integration branch remain denied, while the final remote/main boundary must independently protect trusted nested/custom capabilities.
41
- - The extension never performs merge, deploy, release, or publish at runtime. Remote/main integration and repository releases cross independent protected boundaries.
42
- - A 4-hour wall-clock and 20-minute no-output watchdog stop a worker by default for long development tasks; embedding callers can set either to `0` to disable.
43
- - On Linux, the adapter automatically uses a writable cgroup v2 for descendant cleanup, including `setsid()` descendants; it falls back to process-group cleanup when unavailable. Use `cgroupMode: "required"` for a fail-closed integration; required mode is preflighted before Claude starts.
44
- - Acceptance commands, repository evidence collection and independent Review share an abort signal, so operator stop/shutdown does not wait for a full command or model timeout.
45
- - Events are append-only JSONL records in `~/.pi/agent/claude-supervisor/events.jsonl`.
46
-
47
- ## Install
48
-
49
- Requires Pi 0.85+ and Node.js 22.19+.
9
+ ## What it is
10
+
11
+ pi-claude-supervisor is a standard [Pi](https://pi.dev) agent extension package that
12
+ supervises Claude Code for unattended local development. Pi owns the task's
13
+ lifecycle, state machine, policy decisions, acceptance checks, and an
14
+ independent Review step; Claude Code is the Worker that does the editing. The
15
+ extension is a single `pi install`: Pi discovers and loads it itself, there is
16
+ no separate build or binary, and there is nothing to configure inside Pi
17
+ beyond environment variables.
18
+
19
+ There is a hard boundary the Worker can never cross, regardless of its own
20
+ permission settings: it may not push to a remote, merge into `main` or an
21
+ integration branch, open a pull request, or run a remote CLI mutation;
22
+ `.git` metadata writes and destructive rewrites of protected branches are
23
+ denied outright. Everything else — edits, tests, shell commands, local
24
+ commits — follows the policy you configure. The extension itself never
25
+ merges, deploys, releases, or publishes anything at runtime.
26
+
27
+ How the loop works, once a task starts:
28
+
29
+ - The Worker works a turn; when it stops (`turn_completed`), Pi's Decision
30
+ Worker — a persistent Pi session with read-only tools — chooses to
31
+ continue, redirect, answer a question, verify, stop, or park the task.
32
+ - `verify` runs the acceptance checks (the default `git diff --check`, or the
33
+ checks from a `--spec` file).
34
+ - An independent Reviewer — a fresh, read-only Pi session — returns pass,
35
+ revise, or human.
36
+ - `revise` sends the Worker a bounded repair turn (up to `maxRepairRounds`);
37
+ a pass promotes the work to a `completed` candidate.
38
+ - Unresolvable work is parked as `blocked` (a non-publishable candidate, not
39
+ a crash); crashes and timeouts become `failed`.
40
+ - Every terminal state emits a candidate notice, in the Pi UI and optionally
41
+ to a webhook (WeCom or generic JSON, with retries).
42
+
43
+ ## Quick start
44
+
45
+ Requirements: Pi 0.85+, Node.js 22.19+, Claude Code 2.1.270+ (interactive
46
+ hooks verified on 2.1.273), Linux for the cgroup and tmux features.
47
+
48
+ Install it like any other Pi extension:
50
49
 
51
50
  ```text
52
51
  pi install npm:pi-claude-supervisor
53
52
  ```
54
53
 
55
- For local development:
54
+ That's the whole installation — Pi loads the package's `./src/index.ts`
55
+ extension directly and registers the `/supervise` command. Configuration is
56
+ environment variables only, set before starting Pi (or in
57
+ `~/.config/pi-claude-supervisor/env`):
56
58
 
57
59
  ```bash
58
- npm ci --ignore-scripts
59
- npm run check
60
- npm run build
60
+ export PI_CLAUDE_SUPERVISOR_MODE=auto
61
+ export PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux
62
+ # Optional: candidate/failure notifications
63
+ export PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_URL='https://example.invalid/webhook'
64
+ export PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_FORMAT=generic
61
65
  ```
62
66
 
63
- Authenticated real-Claude spikes resolve `claude` from `PATH` by default, so
64
- installer-managed `latest` links work without a versioned path. Set
65
- `PI_CLAUDE_SUPERVISOR_REAL_CLAUDE_PATH` only when an explicit executable is needed;
66
- the spikes require Claude Code `2.1.270` or newer and report the resolved path and version.
67
-
68
- ## Use
67
+ Then, inside any Pi session:
69
68
 
70
- Set the worker executable if needed, then use explicit commands in Pi:
71
-
72
- ```bash
73
- export PI_CLAUDE_SUPERVISOR_WORKER=claude
69
+ ```text
70
+ /supervise start implement the requested change
71
+ /supervise adopt-tmux my-tmux-session implement the requested change
74
72
  ```
75
73
 
74
+ Watch a task with `/supervise status <task-id>` or `/supervise sessions`; for
75
+ a tmux transport, attach directly with the `tmux -S <socket> attach -t
76
+ <session>` command each of these prints. `/supervise stop <task-id>` closes
77
+ the task; on the interactive tmux transport a completed task instead leaves
78
+ the session open for you by default (see below).
79
+
80
+ ## Modes and transports
81
+
82
+ `PI_CLAUDE_SUPERVISOR_MODE=auto` (or `PI_CLAUDE_SUPERVISOR_AUTOMATION=1`)
83
+ enables automatic supervision — the Decision Worker/Reviewer loop above.
84
+ Without it, `/supervise` still exposes its commands, but a Worker runs
85
+ without that loop.
86
+
87
+ | Transport | `TRANSPORT` | `TMUX_MODE` | What the Worker runs as | Use it when |
88
+ | --- | --- | --- | --- | --- |
89
+ | Interactive tmux | `tmux` | `interactive` (default) | The real, unmodified Claude Code TUI in a tmux pane, driven by Claude Code hooks | You want to watch or occasionally type into the exact session Claude uses |
90
+ | Headless JSONL | `jsonl` (default in `auto` mode) | – | `claude -p --input-format stream-json`, no terminal | Every permission-relevant command must be visible to the Supervisor |
91
+ | tmux bridge | `tmux` | `bridge` | Claude's stream-json protocol, rendered into a tmux pane | A visible pane with the older structured (pre-hook) transport |
92
+ | Manual (process-pipe) | `process-pipe` (default when `MODE` is unset) | – | The worker's stdin/stdout as plain text | Exposing the commands without automatic supervision |
93
+
94
+ ## Interactive tmux mode (hooks)
95
+
96
+ `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux` defaults to running the real,
97
+ unmodified Claude Code TUI in the tmux pane — the same interface you would
98
+ see running `claude` yourself — instead of the structured stream-json
99
+ bridge. You can attach to the printed `attach=...` command at any time and
100
+ watch, or type into the session yourself; Pi reports its events through
101
+ Claude Code's own hooks rather than scraping the screen.
102
+
103
+ When the extension loads with `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux` in
104
+ interactive mode (the default `TMUX_MODE`), it automatically installs a small
105
+ relay command for the eight Claude Code hook events it uses into
106
+ `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`) — idempotent,
107
+ and announced once in the Pi UI. Set
108
+ `PI_CLAUDE_SUPERVISOR_AUTO_INSTALL_HOOKS=0` to opt out; `/supervise
109
+ uninstall-hooks` removes the entry, and `/supervise install-hooks` remains
110
+ available to install it by hand. The relay is a ~1ms no-op in any Claude
111
+ session no Supervisor is listening for. An owned `/supervise start` does not
112
+ depend on this at all — it passes its own `--settings` file — but
113
+ `/supervise adopt-tmux` runs inside your normal Claude Code configuration and
114
+ needs the relay installed there.
115
+
76
116
  ```text
77
- /supervise capabilities
78
- /supervise start inspect the current repository and report what should be changed
79
- /supervise start --spec ./task.json
80
- /supervise sessions
81
- /supervise recover [--takeover] <task-id>
82
- /supervise poll
83
- /supervise poll all
84
- /supervise send continue with read-only inspection
85
- /supervise pause
86
- /supervise resume
87
- /supervise stop human requested stop
88
- /supervise verify
117
+ /supervise start <task>
118
+ /supervise adopt-tmux <tmux-session> <task>
89
119
  ```
90
120
 
91
- `--spec` accepts a JSON file; checks are always executed with argv (never through
92
- a shell), for example:
121
+ Pi intervenes when Claude stops a turn (`Stop`; an API/model failure mid-turn
122
+ arrives as `StopFailure` and is treated as an errored turn the Decision
123
+ Worker can retry, and a minute of idle prompt with no stop signal closes the
124
+ turn as a safety net), when Claude is about to show you a real permission
125
+ prompt (only then — every ordinary tool call is otherwise left to your own
126
+ Claude Code permission mode), when Claude asks `AskUserQuestion` (the
127
+ Decision Worker picks an answer and Claude continues with it as ordinary
128
+ text, exactly as in headless mode), and when the session exits. Before that
129
+ prompt, a `PreToolUse` veto point only ever denies a known direct remote
130
+ push/merge/PR or other destructive/protected-branch operation (or forwards an
131
+ `AskUserQuestion`); it never second-guesses a normal edit, read, or local
132
+ command — those reach your own permission mode with no decision from Pi at
133
+ all.
134
+
135
+ **What this means for the boundary.** Interactive mode deliberately skips the
136
+ headless-mode check that refuses inherited `Bash` pre-authorization or an
137
+ `auto`/`bypassPermissions` mode in your Claude settings: your own
138
+ configuration governs what Claude may do without asking, exactly as when you
139
+ run Claude yourself. Anything your settings already allow never reaches the
140
+ Decision Worker; its judgment applies only where Claude would have asked
141
+ *you*. The hard boundary (remote push/merge/PR, remote CLI mutation, `.git`
142
+ writes, destructive protected-branch rewrites) is enforced by `PreToolUse`
143
+ regardless of permission mode — verified against `auto` mode on Claude Code
144
+ 2.1.273 — and is the only guarantee this mode makes beyond your own
145
+ settings. Use headless (`bridge`) mode when every `Bash` call must be visible
146
+ to the Supervisor.
147
+
148
+ **Adopting an idle session.** `adopt-tmux` types the task into the session
149
+ only when Claude is idle at its prompt; a session caught mid-turn keeps its
150
+ current work and is judged on its next `Stop` instead.
151
+
152
+ **Human coexistence.** If you type into the attached session, automation
153
+ pauses (`human_takeover`, visible as a warning) until you run `/supervise
154
+ resume-auto <task-id>`; the turn that completed while you were driving is
155
+ replayed to the Decision Worker at that point, so nothing already finished is
156
+ lost.
157
+
158
+ **Completion hands the session back.** Unlike other transports, a completed
159
+ task by default disconnects Pi from the session instead of closing it, so you
160
+ can keep working in the same window or review what Claude did; `/supervise
161
+ stop <task-id>` closes it explicitly, and a blocked or failed candidate still
162
+ stops the Worker as usual. Set
163
+ `PI_CLAUDE_SUPERVISOR_CLOSE_WORKER_ON_COMPLETION=1` to restore the old
164
+ close-on-completion behavior. The hand-back is clean, not a bare disconnect:
165
+ before reporting the candidate ready, Pi moves every process out of its
166
+ private cgroup into its parent (instead of killing them) and stops the
167
+ guardian process, and the tmux server and pane are left alone — the session
168
+ survives a Pi restart with nothing left owing it. Afterward it is an ordinary
169
+ tmux session with no Supervisor attached; `tmux -S <socket> attach -t
170
+ <session>` reaches it directly, and `/supervise adopt-tmux` can babysit it
171
+ again exactly as it would any other externally created session.
172
+
173
+ **Cost accounting limits.** The TUI's `Stop` hook has no `total_cost_usd` or
174
+ token `usage` (that only comes from Claude's own `result` stream-json record,
175
+ which the TUI does not emit), so cost tracking in interactive mode only
176
+ counts turns, not dollars; `--max-budget-usd` is also unavailable (Claude
177
+ Code only enforces it under `-p`) and is not passed to an interactive launch.
178
+ Set `autonomy.maxWorkerCostUsd` expecting it to have no effect in interactive
179
+ mode, or use bridge/jsonl mode when a hard cost cap matters.
180
+
181
+ **Trust dialog.** The very first time Claude Code runs in a given directory
182
+ it shows its own one-time "do you trust this folder" dialog before any hook
183
+ fires. For a session that `/supervise start` launched, the launcher accepts
184
+ it automatically — only when the pane's directory is the task directory. An
185
+ adopted session was started by you, so you already answered it.
186
+
187
+ ## Headless mode (JSONL)
188
+
189
+ `PI_CLAUDE_SUPERVISOR_TRANSPORT=jsonl` runs Claude as `claude -p
190
+ --input-format stream-json`, with no terminal at all; it is the default
191
+ transport once `PI_CLAUDE_SUPERVISOR_MODE=auto` is set. Every permission
192
+ request Claude makes is answered by the Supervisor — the deny list, routine
193
+ in-cwd edits, and read-only/local-dev shell commands are answered by policy
194
+ alone, and everything else goes to the Decision Worker. `worker_usage`
195
+ events carry full token and cost accounting from Claude's own `result`
196
+ records, so `--max-budget-usd` and the token/cost reporting below both work
197
+ as expected. Use this transport when nothing should ever run without the
198
+ Supervisor being able to see it, or when you don't need to attach.
199
+
200
+ ## Safety boundary
201
+
202
+ - Always denied, regardless of policy or permission mode: remote push,
203
+ merge/PR into `main` or an integration branch, other remote CLI mutations,
204
+ `.git` metadata writes, and destructive rewrites of protected branches
205
+ (`reset`, `update-ref`, `symbolic-ref`, or a delete/move/force `branch`).
206
+ A shell argument the policy cannot see through (`$VAR`, `$(…)`, a glob)
207
+ is vetoed, best-effort, only on the commands where it could reach that
208
+ boundary — git, gh, npm/pnpm/yarn, curl/wget/ssh, a nested `claude`, or an
209
+ interpreter/runner such as `eval`, `sh -c`, `xargs`, `find -exec`. A quoted
210
+ heredoc body is judged by its consumer: a shell runs it, `cat > file` or
211
+ `git commit -m` stores it. Everything else (`for f in …; do echo "$f"`,
212
+ `rm -rf ./dist`, a Write to Claude's own scratchpad) follows the configured
213
+ policy — Claude's own permission mode governs it, as when you run Claude.
214
+ - `autonomy.permissionAuthority` (`policy` | `hybrid` default |
215
+ `decision-worker`) controls who answers a permission request — every
216
+ request in headless mode, and in interactive tmux mode only those Claude
217
+ would otherwise have shown you as a prompt: `hybrid` answers routine
218
+ in-cwd edits and local read-only/dev shell commands from policy alone, and
219
+ sends every ambiguous request to the Decision Worker (a policy denial is
220
+ always applied directly).
221
+ - **Baseline, not branch.** Any branch, including `main`, may be supervised;
222
+ the candidate only has to descend from the recorded baseline commit
223
+ (`merge-base --is-ancestor`). A branch change mid-task is recorded
224
+ (`worker_branch_changed`), not rejected, and a candidate on a protected
225
+ branch is reported in its notice (`branch`, `protectedBranch`), not parked.
226
+ `checkout`/`switch` onto `main` is allowed; only a destructive rewrite of a
227
+ protected branch name is denied. Claude Code's own "branch first if you're
228
+ on the default branch" guidance is advisory, not enforced.
229
+ - Worker commands launch without a shell. Automatic mode admits only the bare
230
+ `claude` command name and pins an operator-owned, non-writable executable
231
+ path (`PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE` to pin one explicitly).
232
+ - On Linux, a cgroup v2 boundary cleans up every descendant, including
233
+ `setsid()` descendants; `required` mode fails closed instead of falling
234
+ back.
235
+ - A 4-hour wall-clock deadline and a 20-minute no-output watchdog stop a
236
+ worker by default; embedding callers can change or disable either
237
+ (`deadlineMs`, `noOutputTimeoutMs`).
238
+ - Acceptance checks, evidence collection, and the Reviewer share an abort
239
+ signal, so a stop or shutdown does not wait for a full command or model
240
+ timeout.
241
+ - Only one cwd lease is held per task; concurrent tasks need separate
242
+ worktrees.
243
+
244
+ ## Task specs
245
+
246
+ `--spec file.json` accepts:
93
247
 
94
248
  ```json
95
249
  {
@@ -98,195 +252,201 @@ a shell), for example:
98
252
  "constraints": ["Keep the public API compatible"],
99
253
  "forbidden": ["Do not publish artifacts"],
100
254
  "acceptance": [
101
- { "id": "tests", "name": "tests", "command": "npm", "args": ["test"], "required": true }
255
+ { "id": "tests", "name": "tests", "command": "npm", "args": ["test"], "required": true, "timeoutMs": 120000 }
102
256
  ],
103
257
  "maxRepairRounds": 3,
104
258
  "autonomy": {
105
259
  "unattended": true,
106
260
  "requireLocalCommit": true,
107
- "maxDecisionRetries": 2
261
+ "maxDecisionRetries": 2,
262
+ "permissionAuthority": "hybrid",
263
+ "maxWorkerCostUsd": 20
108
264
  }
109
265
  }
110
266
  ```
111
267
 
112
- The default MVP writes the task to the worker's stdin as plain process-pipe
113
- text. After running the transport spike for the target CLI, JSONL framing can
114
- be selected explicitly:
115
-
116
- ```bash
117
- export PI_CLAUDE_SUPERVISOR_TRANSPORT=jsonl
118
- export PI_CLAUDE_SUPERVISOR_WORKER='claude --permission-mode acceptEdits'
119
- ```
120
-
121
- This adds Claude Code stream-json flags and frames supervisor messages as JSONL.
122
- The supervisor allows only one active JSONL request per session: poll until its
123
- `result` and `waiting` state before sending the next turn. Session resume is not
124
- yet exposed by the adapter. Multiple independent task sessions can run
125
- concurrently when they use different canonical working directories or
126
- worktrees; same-directory starts are rejected even when concurrent, and
127
- `/supervise sessions` lists the sessions. This is independent-session
128
- parallelism, not coordinated multi-worker collaboration. A future multi-worker
129
- milestone will add explicit parent/child task graphs, dependencies, bounded
130
- scheduling, structured handoffs, aggregate acceptance and graph-aware recovery;
131
- it will not grant any Worker remote push or main/integration merge authority.
132
- Unattended local development is the target operating mode; a blocked or failed
133
- candidate is parked with its evidence rather than made dependent on a human being
134
- online. Automatic mode preserves Claude Code's normal argument and extension surface:
135
- its tools, agents, background tasks, plugins, MCP configuration, credentials and network
136
- access are not replaced with a sandbox or allowlist. `CLAUDECODE` is removed from the
137
- Worker environment so nested Claude sessions can start, and the Supervisor-owned cgroup
138
- still cleans every descendant. Automatic mode adds a safe `default` permission mode
139
- when omitted and rejects Bash preauthorization in the effective CLI/settings roots
140
- (including an overridden `HOME`); Bash itself remains available through
141
- Supervisor-visible permission requests. The automatic tmux bridge repeats that
142
- settings check synchronously immediately before spawning Claude, so a mutation after
143
- Supervisor preflight fails closed. The adapter also adds stream-json transport framing,
144
- and its known direct command policy refuses remote push/main integration operations.
145
- Automatic mode still accepts only the bare direct Claude command name, pins its
146
- operator-owned resolved executable path, and rejects explicit executable paths or
147
- writable/untrusted locations. Set `PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE` when the resolved
148
- path must be pinned explicitly. Custom tools and nested workers are trusted capabilities,
149
- so a hard remote/main boundary must remain independently protected outside this process.
150
- Set `PI_CLAUDE_SUPERVISOR_REQUIRE_LOCAL_COMMIT=0` only for a task that intentionally
151
- produces no local commit candidate, or set `autonomy.requireLocalCommit` in its spec;
152
- automatic mode still requires a valid Git baseline and non-protected worktree.
153
- `PI_CLAUDE_SUPERVISOR_UNATTENDED=0` opts a task out of automatic Decision Worker control;
154
- a required local commit is checked on a non-protected task branch;
155
- `PI_CLAUDE_SUPERVISOR_MAX_DECISION_RETRIES` bounds transient Decision Worker retries.
156
-
157
- The `v0.5.0` automation milestone adds a structured acceptance pipeline:
158
- multiple argv-based checks, an independent read-only Reviewer, bounded structured
159
- findings and repair rounds. Legacy text tasks keep the default `git diff --check`.
160
- The Reviewer only has `read`, `grep`, `find` and `ls`; it cannot edit files or grant
161
- permissions. Automatic mode requires complete baseline-relative tracked, commit and
162
- bounded untracked evidence, repairs a live `repairableSession` Worker within a
163
- bounded budget, enforces a local commit when enabled, and prevents a non-publishable
164
- candidate from crossing the remote/main boundary. Automatic mode rejects explicit
165
- `process-pipe` and preflights runtime prerequisites. Invalid output, unavailable
166
- evidence, duplicate findings, P0/P1 findings and exhausted repair budgets park the
167
- candidate without waiting for a human; see [the autonomy target](docs/autonomy-target.md).
168
- The automatic tmux bridge carries Claude stream-json records inside the same live PTY
169
- as display output using private terminal framing; it does not create an independent
170
- structured-event sidecar. Automatic tmux accepts only Supervisor-owned sessions,
171
- while `adopt-tmux` remains manual-only.
172
- Coordinated multi-worker scheduling is a later milestone; CI uses deterministic fake
173
- Workers/replay fixtures, and real multi-worker Claude tests remain authenticated
174
- manual Spikes. Full host-level sandboxing and low-privilege execution for custom
175
- integrations remain separate hardening work.
176
-
177
- Automatic mode persists the Pi Decision Worker session under the configured state
178
- directory. After an unclean Pi restart, `/supervise sessions` lists recoverable
179
- tasks; `/supervise recover [--takeover] <task-id>` explicitly restores the Decision Worker
180
- context and starts a new Claude Worker. It never silently resumes or duplicates
181
- work. If the old Pi owner is dead, add `--takeover` only after the lease proves
182
- the old Worker's process group is gone and its cgroup is a real, readable empty
183
- boundary; missing or unverifiable Worker evidence is refused. The lease also
184
- persists the generated Worker/cgroup identity, including the cgroup device/inode,
185
- and rejects a renamed or replaced cgroup. Automatic Workers retain a verified
186
- empty cgroup until the owning cwd lease is released, covering a normal-exit
187
- crash between Worker cleanup and lease finalization; release then removes it.
188
- Automatic lease acquisition records a no-spawn startup marker and the adapter
189
- persists its generated cgroup/socket plan before creating those resources,
190
- then records cgroup and server identity in stages before spawn. Recovery
191
- inspects and cleans a planned empty cgroup/session instead of assuming that
192
- startup-only means no resource exists. A stale marker is reclaimable only after
193
- its owner is proven dead because that adapter has not reached spawn. For an automatic tmux lease, takeover additionally requires Supervisor ownership,
194
- dead tmux-server identity, a gone private tmux session, and a durable
195
- cleanup-pending transaction plus atomic reservation of the private socket. The
196
- replacement reuses the old lease record, and the reservation remains until that
197
- replacement is written. Only then is the guardian-left-empty cgroup removed; a
198
- fresh Supervisor can reconcile the pending transaction if recovery is
199
- interrupted. For a persistent manual tmux Worker, use explicit `adopt-tmux`
200
- instead of takeover.
201
- Manual embedding integrations may pass credentials through an explicit
202
- `WorkerStartInput.env`. Automatic mode passes the full supervisor environment to the
203
- Worker (except `CLAUDECODE`), including provider/remote credentials, credential helpers,
204
- configuration and proxy settings. Keep the Supervisor's own environment appropriate for
205
- the task; `PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE` pins executable identity but is otherwise
206
- not used to select the Claude binary.
207
-
208
- ### tmux/PTY transport
209
-
210
- For an interactive Claude Code window, opt in to the tmux transport:
211
-
212
- ```bash
213
- export PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux
214
- export PI_CLAUDE_SUPERVISOR_WORKER='claude'
215
- # Manual tmux currently requires Linux for process identity and cleanup;
216
- # it may use cgroup mode auto/off.
217
- # Set PI_CLAUDE_SUPERVISOR_MODE=auto for the Supervisor-owned automatic bridge;
218
- # automatic tmux additionally requires Linux cgroup v2 and a parent-death guardian.
219
- # Optional, only when adopting a non-default tmux server:
220
- # export PI_CLAUDE_SUPERVISOR_TMUX_SOCKET=/path/to/tmux.sock
221
- ```
222
-
223
- `/supervise start <task>` starts Claude in a private tmux server and reports a
224
- literal attach command. Use that command in another terminal to watch or
225
- manually interact with the same PTY; attaching is optional for unattended local
226
- development. The adapter sends multi-line input through
227
- tmux buffers and Enter, never by interpolating the message into a shell command.
228
- In automatic mode the Supervisor starts a bridge in the pane: it runs Claude's
229
- stream-json protocol inside the live PTY, contains the bridge and descendants in
230
- an owned cgroup, fails closed when that containment or the Linux guardian is
231
- unavailable, renders readable deltas for the attached terminal, and returns
232
- structured records through private terminal framing on the same PTY. The bridge
233
- re-reads effective Claude settings in the same process immediately before its
234
- child `spawn`, so a startup settings mutation fails closed instead of reaching
235
- an unchecked Claude process. The adapter parses those records from the raw PTY
236
- pipe, so there is no independent
237
- JSONL event sidecar. Automatic mode refuses adopted sessions; use JSONL or the owned
238
- bridge for unattended decisions, repair and protected command enforcement.
239
-
240
- A session that you started yourself can be explicitly adopted without replaying
241
- the task:
242
-
243
- ```text
244
- /supervise adopt-tmux <tmux-session-name> <task description>
245
- ```
246
-
247
- Adoption checks the session's working directory and pane command, and refuses a
248
- pane that already has another output pipe. Owned sessions use a generated
249
- private tmux socket, so preserve the complete `attach=...` command printed by
250
- `start`. When re-adopting after a Pi restart, set
251
- `PI_CLAUDE_SUPERVISOR_TMUX_SOCKET` to the socket path from that command before
252
- running `adopt-tmux`; the session name alone is sufficient only for the default
253
- server. Adoption does not claim ownership: `/supervise stop` and Pi shutdown
254
- detach supervision rather than killing the user's tmux session. Use `tmux
255
- kill-session` yourself when the adopted window should be closed.
256
- `/supervise takeover <task-id>` disables automatic Decision Worker messages;
257
- resume them only with `/supervise resume-auto <task-id>`.
258
-
259
- PTY screen text is not itself Claude JSONL and must not be treated as structured
260
- permission evidence; only the Supervisor bridge's private framed records are
261
- authoritative. TUI decisions follow the configured autonomy policy and are
262
- recorded; an unresolved task may be parked without requiring a human to remain
263
- online. A normal terminal Claude process cannot be migrated into tmux, and
264
- `--resume` is historical recovery rather than live PTY attach. Manual owned tmux
265
- sessions survive a Pi disconnect and require an explicit `adopt-tmux` after
266
- restart; automatic owned sessions are terminated by their parent-death guardian
267
- when the Supervisor disappears. A later `/supervise recover --takeover` may
268
- reclaim an automatic lease only after the guardian, process, cgroup and private
269
- tmux-session proofs pass. Use plan/read-only flags for live testing.
268
+ Checks always run with argv, never through a shell. A plain-text task (no
269
+ `--spec`) becomes a `goal` with the default `git diff --check` acceptance
270
+ check (120s timeout) and the env autonomy defaults below.
271
+
272
+ ## Configuration reference
273
+
274
+ Environment variables (or `~/.config/pi-claude-supervisor/env`), all prefixed
275
+ `PI_CLAUDE_SUPERVISOR_`; see `.env.example` for a template.
276
+
277
+ | Variable | Default | Meaning |
278
+ | --- | --- | --- |
279
+ | `MODE` | unset (manual) | `auto` enables automatic supervision (Decision Worker + Reviewer loop) |
280
+ | `AUTOMATION` | unset | `1` is equivalent to `MODE=auto` |
281
+ | `TRANSPORT` | `jsonl` in auto mode, `process-pipe` otherwise | `jsonl` \| `tmux` \| `process-pipe` (manual only) |
282
+ | `TMUX_MODE` | `interactive` | `interactive` (real TUI via hooks) \| `bridge` (stream-json in a pane) |
283
+ | `AUTO_INSTALL_HOOKS` | `true` | Interactive tmux mode only: automatically install the hook relay into the user's Claude settings on load; `0` opts out |
284
+ | `CLOSE_WORKER_ON_COMPLETION` | `false` | Interactive tmux only: close the Worker/session on completion instead of leaving it open |
285
+ | `CGROUP_MODE` | `auto` | `off` \| `auto` \| `required`; automatic mode always uses `required` on Linux; `required` is rejected for a manual (non-automatic) tmux Worker |
286
+ | `TMUX_SOCKET` | unset (default tmux server) | Socket path for adopting a non-default tmux server |
287
+ | `WORKER` | `claude` | Worker command; may include arguments |
288
+ | `NODE` | unset (resolved from `PATH`) | Explicit `node` executable path, for a Bun-compiled Pi |
289
+ | `TRUSTED_CLAUDE` | unset | Pins the expected resolved Claude executable identity explicitly |
290
+ | `STATE_DIR` | `~/.pi/agent/claude-supervisor` | Supervisor state directory |
291
+ | `CWD_LEASE_DIR` | `<state>/cwd-leases` | Shared cwd-lease registry directory |
292
+ | `WORKER_ENV` | unset | Comma-separated list of env vars to pass through to manual workers |
293
+ | `HUMAN_WEBHOOK_URL` | unset | Outbound candidate/failure notification endpoint |
294
+ | `HUMAN_WEBHOOK_FORMAT` | `generic` | `wecom` \| `generic` |
295
+ | `HUMAN_WEBHOOK_SECRET` | unset | HMAC signing secret; sent as the `x-pi-supervisor-signature` header |
296
+ | `UNATTENDED` | `true` | Task runs without a synchronous human callback |
297
+ | `REQUIRE_LOCAL_COMMIT` | `true` | Require a local commit on the candidate's branch before completion |
298
+ | `MAX_DECISION_RETRIES` | `2` (0–10) | Retries of a Decision Worker call that times out or fails (429/529, network, auth) |
299
+ | `PERMISSION_AUTHORITY` | `hybrid` | `policy` \| `hybrid` \| `decision-worker` |
300
+ | `WORKER_MAX_BUDGET_USD` | unset | Hard cap passed as `--max-budget-usd`; unavailable to interactive tmux |
301
+ | `WORKER_MODEL` | unset (Claude's own default) | `--model` for the Claude Worker |
302
+ | `WORKER_AUTOCOMPACT_TOKENS` | `200000` in automatic mode | Per-turn context bound; `0` keeps Claude's own default |
303
+ | `WORKER_MCP_CONFIG` | unset | Path passed as `--strict-mcp-config --mcp-config`, restricting the Worker's MCP servers |
304
+ | `DECISION_MODEL` | unset (Pi's default) | `provider/model-id` for the Pi Decision Worker, as listed by Pi |
305
+ | `REVIEWER_MODEL` | unset (Pi's default) | `provider/model-id` for the independent Reviewer |
306
+ | `DECISION_COMPACT_TOKENS` | `60000` | Proactively compacts the persistent Decision Worker session past this size; `0` disables it |
307
+ | `PROGRESS_HEARTBEAT_MS` | `60000` | Minimum interval between repeated progress notifications for the same phase |
308
+ | `DECISION_SESSION_RETENTION_DAYS` | `30` | Prunes closed Decision Worker session records older than this; `0` keeps forever |
309
+ | `EVIDENCE_MAX_BYTES` | `1048576` (1 MiB) | Maximum repository evidence bytes collected per task |
310
+ | `EVIDENCE_MAX_UNTRACKED_FILES` | `512` | Maximum untracked files collected as evidence per task |
311
+ | `REVIEW_TIMEOUT_MS` | `600000` (10 min) | Total independent Reviewer budget per round, including one retry on a provider error |
312
+ | `EVENT_LOG_MAX_BYTES` | `67108864` (64 MiB) | Rotates `events.jsonl` at this size; 5 rotated files are kept |
313
+
314
+ ## Recovery, leases and state
315
+
316
+ After an unclean Pi restart, `/supervise sessions` lists recoverable tasks;
317
+ `/supervise recover [--takeover] <task-id>` restores the Decision Worker
318
+ context and starts a new Claude Worker. It never silently resumes or
319
+ duplicates work. Add `--takeover` only once the lease proves the old
320
+ Worker's process group is gone and its cgroup is a real, readable empty
321
+ boundary (and, for tmux, that the private tmux session is also gone);
322
+ missing or unverifiable evidence is refused rather than reclaimed.
323
+ `/supervise recover` does not persist whether the original task was
324
+ interactive — it derives that from the current `TRANSPORT`/`TMUX_MODE`
325
+ configuration at recovery time, so do not change either between starting a
326
+ task and recovering it.
327
+
328
+ Every task holds one cwd lease under `CWD_LEASE_DIR`; concurrent tasks need
329
+ separate worktrees. A lease record that cannot be read (corrupt JSON,
330
+ unexpected shape) is quarantined instead of blocking other lookups, and
331
+ `/supervise sessions` lists the current quarantined records so an operator
332
+ can inspect and clean them up.
333
+
334
+ Events are append-only JSONL in `<STATE_DIR>/events.jsonl`, with
335
+ `worker_output` size-capped and the log rotated past `EVENT_LOG_MAX_BYTES`.
336
+ The Decision Worker session for each task is persisted as its own JSONL file
337
+ under the state directory, pruned by `DECISION_SESSION_RETENTION_DAYS`.
338
+
339
+ ## Notifications
340
+
341
+ Every terminal state (`completed`, `blocked`, `failed`) emits a candidate
342
+ notice in the Pi UI and, if `HUMAN_WEBHOOK_URL` is set, to a webhook in
343
+ either `wecom` or `generic` JSON form, signed with `HUMAN_WEBHOOK_SECRET`
344
+ when set. A parked candidate that asked a question sends a separate "needs
345
+ you" notice; a human takeover (you typed into the session) only shows an
346
+ in-UI hint, since you are already there. Either notice includes an
347
+ `attach` field with the literal `tmux -S <socket> attach -t <session>`
348
+ command when it concerns a tmux session, and a usage summary
349
+ (`CandidateNotice.usage`: cost, worker turns/tokens, Pi tokens, decision and
350
+ reviewer call counts). Webhook delivery retries transient errors (network,
351
+ 429/5xx). Notifications are outbound-only: receiving one does not grant any
352
+ approval, and a webhook cannot push commands back into Pi — use `/supervise
353
+ send`/`approve`/`takeover` for that.
354
+
355
+ ## Token usage and cost controls
356
+
357
+ Measured on one real unattended review task (29 minutes wall clock):
358
+
359
+ | Component | Turns/calls | Tokens | Cost |
360
+ | --- | --- | --- | --- |
361
+ | Claude Code Worker | 70 turns | 15.5M cache-read + 370k cache-write + 100k output | $18.46 |
362
+ | Pi Decision Worker | 30 model calls | ~1.0M (91k uncached + 914k cache-read) | $0.04 |
363
+
364
+ Almost all of the money goes to the Worker, not the Supervisor's own Decision
365
+ Worker or Reviewer calls. In this run the Worker averaged ~220k tokens of
366
+ context per turn because it ran as a single long `-p` session under a
367
+ 1M-token window that never compacted; a trivial Claude Code turn costs
368
+ roughly 24k prompt tokens for its system prompt alone, regardless of which
369
+ MCP servers are configured. Of the 30 Decision Worker calls, 28 were
370
+ permission requests, and the Decision Worker overrode the deterministic
371
+ policy 4 times (denying downloads and writes outside the task directory) —
372
+ this is why `hybrid` is the default `permissionAuthority`, not `policy`.
373
+ Replaying those 28 requests through the shipped `isRoutinePermission`
374
+ classifier answers 4 of them locally; that task was dominated by inline
375
+ `node -e` scripts and `$(...)` substitutions, which are never routine. An
376
+ ordinary implementation task is mostly in-cwd `Edit`/`Write`, `npm test` and
377
+ `git status/diff/add/commit`, all of which are routine, so its Decision
378
+ Worker call count drops much further.
379
+
380
+ Knobs, with their defaults and trade-offs:
381
+
382
+ - `PERMISSION_AUTHORITY` (`policy` | `hybrid` default | `decision-worker`):
383
+ `hybrid` answers routine in-cwd file edits and local read-only/dev shell
384
+ commands from the deterministic policy alone (`isRoutinePermission` in
385
+ `src/policy.ts`) and still sends every ambiguous request, and every policy
386
+ denial, to the Decision Worker. This mainly buys latency and a smaller
387
+ Decision Worker context, not dollars: the 30 calls above already cost
388
+ $0.04.
389
+ - `WORKER_MODEL` / `--model`: roughly a 5x price difference between Opus- and
390
+ Sonnet-class models. This is the single largest lever on the actual bill,
391
+ and it is the operator's choice; the Supervisor does not pick it for you.
392
+ - `WORKER_AUTOCOMPACT_TOKENS` (default 200000 in automatic mode; `0` keeps
393
+ Claude's own default): bounds context per Worker turn so a long session
394
+ does not keep accumulating ~220k-token turns. Worth tens of percent, at
395
+ the cost of some context quality.
396
+ - `WORKER_MAX_BUDGET_USD` / `autonomy.maxWorkerCostUsd`: a hard cap passed to
397
+ Claude as `--max-budget-usd` and re-checked by the Supervisor against the
398
+ cumulative Worker `result` cost. It is a cap, not a saving; a task that
399
+ hits it is parked with its evidence.
400
+ - `WORKER_MCP_CONFIG` (`--strict-mcp-config --mcp-config`): restricts the
401
+ Worker to only the listed MCP servers. It bounds what the Worker can
402
+ reach, not the ~24k-token fixed overhead of an ordinary turn.
403
+ - `DECISION_MODEL` / `REVIEWER_MODEL` (`provider/model-id`, for example
404
+ `anthropic/claude-haiku-4-5-20251001`): the Pi Decision Worker and
405
+ Reviewer models. Pi-side usage was already a few cents in this run, so a
406
+ cheaper model here mostly buys latency, not headline savings.
407
+ - `DECISION_COMPACT_TOKENS` (default 60000; `0` disables): proactively
408
+ compacts the persistent Decision Worker session once its estimated context
409
+ passes this threshold, and re-sends the startup instructions once on the
410
+ next prompt after compaction.
411
+
412
+ The Supervisor records what it spends rather than estimating it after the
413
+ fact: every Worker `result` record becomes a `worker_usage` event, every
414
+ Decision Worker/Reviewer model call becomes a `pi_usage` event, and both
415
+ accumulate into `session.usage` (`SupervisorTokenUsage`). `/supervise status
416
+ <task-id>` prints a `cost=… workerTurns=… workerTokens=… piTokens=…
417
+ decisionCalls=… reviewerCalls=…` summary; progress notifications carry
418
+ `SupervisorProgress.costUsd`/`.piTokens`, and a candidate notification
419
+ carries the same summary through `CandidateNotice.usage`, which the generic
420
+ webhook serialises as a numeric `usage` object and the WeCom format renders
421
+ as two extra lines.
422
+
423
+ None of this changes what a task actually costs beyond the Worker model and
424
+ budget choice; the Supervisor-side changes here mainly cut Decision Worker
425
+ tokens and latency, which were cents to begin with. For a cost-sensitive
426
+ unattended run, a reasonable starting point is a Sonnet-class `WORKER_MODEL`,
427
+ an explicit `WORKER_MAX_BUDGET_USD` per task, the default `hybrid` permission
428
+ authority, and a Haiku-class `DECISION_MODEL`.
270
429
 
271
430
  ## Development
272
431
 
432
+ Contributing to the extension itself (not required to use it):
433
+
273
434
  ```bash
274
- npm run typecheck
275
- npm test
276
- npm run check:package
277
- npm run check:docs
278
- npm run check:automation
279
- npm run check:workflows
435
+ npm ci --ignore-scripts
436
+ npm run check
280
437
  npm run build
438
+ npm run test:pi
439
+ npm run test:install
281
440
  ```
282
441
 
283
- See [the engineering plan](docs/engineering-plan.md), [the confirmed autonomy target](docs/autonomy-target.md), [the independent review](docs/independent-review.md),
284
- [architecture](docs/architecture.md), [testing](docs/testing.md), and [releasing](docs/releasing.md).
442
+ Some tests validate the trusted-executable and protected-branch boundaries
443
+ against the real checkout, so they must run from a non-protected branch and
444
+ from a path that is not group/world-writable.
285
445
 
286
- Pull requests are gated by the aggregated `CI / Quality gate`. Release Please
287
- creates version PRs from Conventional Commits; after a maintainer merges one,
288
- `Release` verifies the exact tag commit and publishes the package with npm
289
- provenance through the protected `npm` environment.
446
+ See [architecture](docs/architecture.md), [testing](docs/testing.md), and
447
+ [releasing](docs/releasing.md) for more detail; `docs/autonomy-target.md`
448
+ records the confirmed unattended-development target this project is built
449
+ around.
290
450
 
291
451
  ## License
292
452