pi-claude-supervisor 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.cn.md +342 -148
- package/README.md +394 -241
- package/docs/architecture.md +196 -12
- package/docs/testing.md +5 -4
- package/package.json +1 -1
- package/src/acceptance.ts +7 -1
- package/src/config.ts +110 -0
- package/src/cwd-lease.ts +166 -37
- package/src/decision-session-store.ts +43 -2
- package/src/decision-worker.ts +284 -18
- package/src/events.ts +125 -5
- package/src/hooks/install.ts +187 -0
- package/src/hooks/relay.ts +191 -0
- package/src/hooks/server.ts +260 -0
- package/src/hooks/settings.ts +44 -0
- package/src/hooks/types.ts +83 -0
- package/src/index.ts +267 -26
- package/src/json-extract.ts +49 -0
- package/src/notifications.ts +61 -10
- package/src/pi-model.ts +44 -0
- package/src/policy.ts +394 -21
- package/src/redaction.ts +1 -1
- package/src/reviewer.ts +86 -56
- package/src/supervisor.ts +774 -82
- package/src/types.ts +55 -3
- package/src/verifier.ts +49 -16
- package/src/worker/environment.ts +41 -2
- package/src/worker/process-adapter.ts +32 -7
- package/src/worker/runtime.ts +51 -0
- package/src/worker/tmux-adapter.ts +573 -28
package/README.md
CHANGED
|
@@ -6,90 +6,237 @@
|
|
|
6
6
|
|
|
7
7
|
English · [简体中文](README.cn.md)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
78
|
-
/supervise
|
|
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
|
-
|
|
92
|
-
|
|
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
|
+
Everything else follows the configured policy.
|
|
207
|
+
- `autonomy.permissionAuthority` (`policy` | `hybrid` default |
|
|
208
|
+
`decision-worker`) controls who answers a permission request — every
|
|
209
|
+
request in headless mode, and in interactive tmux mode only those Claude
|
|
210
|
+
would otherwise have shown you as a prompt: `hybrid` answers routine
|
|
211
|
+
in-cwd edits and local read-only/dev shell commands from policy alone, and
|
|
212
|
+
sends every ambiguous request to the Decision Worker (a policy denial is
|
|
213
|
+
always applied directly).
|
|
214
|
+
- **Baseline, not branch.** Any branch, including `main`, may be supervised;
|
|
215
|
+
the candidate only has to descend from the recorded baseline commit
|
|
216
|
+
(`merge-base --is-ancestor`). A branch change mid-task is recorded
|
|
217
|
+
(`worker_branch_changed`), not rejected, and a candidate on a protected
|
|
218
|
+
branch is reported in its notice (`branch`, `protectedBranch`), not parked.
|
|
219
|
+
`checkout`/`switch` onto `main` is allowed; only a destructive rewrite of a
|
|
220
|
+
protected branch name is denied. Claude Code's own "branch first if you're
|
|
221
|
+
on the default branch" guidance is advisory, not enforced.
|
|
222
|
+
- Worker commands launch without a shell. Automatic mode admits only the bare
|
|
223
|
+
`claude` command name and pins an operator-owned, non-writable executable
|
|
224
|
+
path (`PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE` to pin one explicitly).
|
|
225
|
+
- On Linux, a cgroup v2 boundary cleans up every descendant, including
|
|
226
|
+
`setsid()` descendants; `required` mode fails closed instead of falling
|
|
227
|
+
back.
|
|
228
|
+
- A 4-hour wall-clock deadline and a 20-minute no-output watchdog stop a
|
|
229
|
+
worker by default; embedding callers can change or disable either
|
|
230
|
+
(`deadlineMs`, `noOutputTimeoutMs`).
|
|
231
|
+
- Acceptance checks, evidence collection, and the Reviewer share an abort
|
|
232
|
+
signal, so a stop or shutdown does not wait for a full command or model
|
|
233
|
+
timeout.
|
|
234
|
+
- Only one cwd lease is held per task; concurrent tasks need separate
|
|
235
|
+
worktrees.
|
|
236
|
+
|
|
237
|
+
## Task specs
|
|
238
|
+
|
|
239
|
+
`--spec file.json` accepts:
|
|
93
240
|
|
|
94
241
|
```json
|
|
95
242
|
{
|
|
@@ -98,195 +245,201 @@ a shell), for example:
|
|
|
98
245
|
"constraints": ["Keep the public API compatible"],
|
|
99
246
|
"forbidden": ["Do not publish artifacts"],
|
|
100
247
|
"acceptance": [
|
|
101
|
-
{ "id": "tests", "name": "tests", "command": "npm", "args": ["test"], "required": true }
|
|
248
|
+
{ "id": "tests", "name": "tests", "command": "npm", "args": ["test"], "required": true, "timeoutMs": 120000 }
|
|
102
249
|
],
|
|
103
250
|
"maxRepairRounds": 3,
|
|
104
251
|
"autonomy": {
|
|
105
252
|
"unattended": true,
|
|
106
253
|
"requireLocalCommit": true,
|
|
107
|
-
"maxDecisionRetries": 2
|
|
254
|
+
"maxDecisionRetries": 2,
|
|
255
|
+
"permissionAuthority": "hybrid",
|
|
256
|
+
"maxWorkerCostUsd": 20
|
|
108
257
|
}
|
|
109
258
|
}
|
|
110
259
|
```
|
|
111
260
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`
|
|
154
|
-
|
|
155
|
-
`
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
the
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
`
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
261
|
+
Checks always run with argv, never through a shell. A plain-text task (no
|
|
262
|
+
`--spec`) becomes a `goal` with the default `git diff --check` acceptance
|
|
263
|
+
check (120s timeout) and the env autonomy defaults below.
|
|
264
|
+
|
|
265
|
+
## Configuration reference
|
|
266
|
+
|
|
267
|
+
Environment variables (or `~/.config/pi-claude-supervisor/env`), all prefixed
|
|
268
|
+
`PI_CLAUDE_SUPERVISOR_`; see `.env.example` for a template.
|
|
269
|
+
|
|
270
|
+
| Variable | Default | Meaning |
|
|
271
|
+
| --- | --- | --- |
|
|
272
|
+
| `MODE` | unset (manual) | `auto` enables automatic supervision (Decision Worker + Reviewer loop) |
|
|
273
|
+
| `AUTOMATION` | unset | `1` is equivalent to `MODE=auto` |
|
|
274
|
+
| `TRANSPORT` | `jsonl` in auto mode, `process-pipe` otherwise | `jsonl` \| `tmux` \| `process-pipe` (manual only) |
|
|
275
|
+
| `TMUX_MODE` | `interactive` | `interactive` (real TUI via hooks) \| `bridge` (stream-json in a pane) |
|
|
276
|
+
| `AUTO_INSTALL_HOOKS` | `true` | Interactive tmux mode only: automatically install the hook relay into the user's Claude settings on load; `0` opts out |
|
|
277
|
+
| `CLOSE_WORKER_ON_COMPLETION` | `false` | Interactive tmux only: close the Worker/session on completion instead of leaving it open |
|
|
278
|
+
| `CGROUP_MODE` | `auto` | `off` \| `auto` \| `required`; automatic mode always uses `required` on Linux; `required` is rejected for a manual (non-automatic) tmux Worker |
|
|
279
|
+
| `TMUX_SOCKET` | unset (default tmux server) | Socket path for adopting a non-default tmux server |
|
|
280
|
+
| `WORKER` | `claude` | Worker command; may include arguments |
|
|
281
|
+
| `NODE` | unset (resolved from `PATH`) | Explicit `node` executable path, for a Bun-compiled Pi |
|
|
282
|
+
| `TRUSTED_CLAUDE` | unset | Pins the expected resolved Claude executable identity explicitly |
|
|
283
|
+
| `STATE_DIR` | `~/.pi/agent/claude-supervisor` | Supervisor state directory |
|
|
284
|
+
| `CWD_LEASE_DIR` | `<state>/cwd-leases` | Shared cwd-lease registry directory |
|
|
285
|
+
| `WORKER_ENV` | unset | Comma-separated list of env vars to pass through to manual workers |
|
|
286
|
+
| `HUMAN_WEBHOOK_URL` | unset | Outbound candidate/failure notification endpoint |
|
|
287
|
+
| `HUMAN_WEBHOOK_FORMAT` | `generic` | `wecom` \| `generic` |
|
|
288
|
+
| `HUMAN_WEBHOOK_SECRET` | unset | HMAC signing secret; sent as the `x-pi-supervisor-signature` header |
|
|
289
|
+
| `UNATTENDED` | `true` | Task runs without a synchronous human callback |
|
|
290
|
+
| `REQUIRE_LOCAL_COMMIT` | `true` | Require a local commit on the candidate's branch before completion |
|
|
291
|
+
| `MAX_DECISION_RETRIES` | `2` (0–10) | Retries of a Decision Worker call that times out or fails (429/529, network, auth) |
|
|
292
|
+
| `PERMISSION_AUTHORITY` | `hybrid` | `policy` \| `hybrid` \| `decision-worker` |
|
|
293
|
+
| `WORKER_MAX_BUDGET_USD` | unset | Hard cap passed as `--max-budget-usd`; unavailable to interactive tmux |
|
|
294
|
+
| `WORKER_MODEL` | unset (Claude's own default) | `--model` for the Claude Worker |
|
|
295
|
+
| `WORKER_AUTOCOMPACT_TOKENS` | `200000` in automatic mode | Per-turn context bound; `0` keeps Claude's own default |
|
|
296
|
+
| `WORKER_MCP_CONFIG` | unset | Path passed as `--strict-mcp-config --mcp-config`, restricting the Worker's MCP servers |
|
|
297
|
+
| `DECISION_MODEL` | unset (Pi's default) | `provider/model-id` for the Pi Decision Worker, as listed by Pi |
|
|
298
|
+
| `REVIEWER_MODEL` | unset (Pi's default) | `provider/model-id` for the independent Reviewer |
|
|
299
|
+
| `DECISION_COMPACT_TOKENS` | `60000` | Proactively compacts the persistent Decision Worker session past this size; `0` disables it |
|
|
300
|
+
| `PROGRESS_HEARTBEAT_MS` | `60000` | Minimum interval between repeated progress notifications for the same phase |
|
|
301
|
+
| `DECISION_SESSION_RETENTION_DAYS` | `30` | Prunes closed Decision Worker session records older than this; `0` keeps forever |
|
|
302
|
+
| `EVIDENCE_MAX_BYTES` | `1048576` (1 MiB) | Maximum repository evidence bytes collected per task |
|
|
303
|
+
| `EVIDENCE_MAX_UNTRACKED_FILES` | `512` | Maximum untracked files collected as evidence per task |
|
|
304
|
+
| `REVIEW_TIMEOUT_MS` | `600000` (10 min) | Total independent Reviewer budget per round, including one retry on a provider error |
|
|
305
|
+
| `EVENT_LOG_MAX_BYTES` | `67108864` (64 MiB) | Rotates `events.jsonl` at this size; 5 rotated files are kept |
|
|
306
|
+
|
|
307
|
+
## Recovery, leases and state
|
|
308
|
+
|
|
309
|
+
After an unclean Pi restart, `/supervise sessions` lists recoverable tasks;
|
|
310
|
+
`/supervise recover [--takeover] <task-id>` restores the Decision Worker
|
|
311
|
+
context and starts a new Claude Worker. It never silently resumes or
|
|
312
|
+
duplicates work. Add `--takeover` only once the lease proves the old
|
|
313
|
+
Worker's process group is gone and its cgroup is a real, readable empty
|
|
314
|
+
boundary (and, for tmux, that the private tmux session is also gone);
|
|
315
|
+
missing or unverifiable evidence is refused rather than reclaimed.
|
|
316
|
+
`/supervise recover` does not persist whether the original task was
|
|
317
|
+
interactive — it derives that from the current `TRANSPORT`/`TMUX_MODE`
|
|
318
|
+
configuration at recovery time, so do not change either between starting a
|
|
319
|
+
task and recovering it.
|
|
320
|
+
|
|
321
|
+
Every task holds one cwd lease under `CWD_LEASE_DIR`; concurrent tasks need
|
|
322
|
+
separate worktrees. A lease record that cannot be read (corrupt JSON,
|
|
323
|
+
unexpected shape) is quarantined instead of blocking other lookups, and
|
|
324
|
+
`/supervise sessions` lists the current quarantined records so an operator
|
|
325
|
+
can inspect and clean them up.
|
|
326
|
+
|
|
327
|
+
Events are append-only JSONL in `<STATE_DIR>/events.jsonl`, with
|
|
328
|
+
`worker_output` size-capped and the log rotated past `EVENT_LOG_MAX_BYTES`.
|
|
329
|
+
The Decision Worker session for each task is persisted as its own JSONL file
|
|
330
|
+
under the state directory, pruned by `DECISION_SESSION_RETENTION_DAYS`.
|
|
331
|
+
|
|
332
|
+
## Notifications
|
|
333
|
+
|
|
334
|
+
Every terminal state (`completed`, `blocked`, `failed`) emits a candidate
|
|
335
|
+
notice in the Pi UI and, if `HUMAN_WEBHOOK_URL` is set, to a webhook in
|
|
336
|
+
either `wecom` or `generic` JSON form, signed with `HUMAN_WEBHOOK_SECRET`
|
|
337
|
+
when set. A parked candidate that asked a question sends a separate "needs
|
|
338
|
+
you" notice; a human takeover (you typed into the session) only shows an
|
|
339
|
+
in-UI hint, since you are already there. Either notice includes an
|
|
340
|
+
`attach` field with the literal `tmux -S <socket> attach -t <session>`
|
|
341
|
+
command when it concerns a tmux session, and a usage summary
|
|
342
|
+
(`CandidateNotice.usage`: cost, worker turns/tokens, Pi tokens, decision and
|
|
343
|
+
reviewer call counts). Webhook delivery retries transient errors (network,
|
|
344
|
+
429/5xx). Notifications are outbound-only: receiving one does not grant any
|
|
345
|
+
approval, and a webhook cannot push commands back into Pi — use `/supervise
|
|
346
|
+
send`/`approve`/`takeover` for that.
|
|
347
|
+
|
|
348
|
+
## Token usage and cost controls
|
|
349
|
+
|
|
350
|
+
Measured on one real unattended review task (29 minutes wall clock):
|
|
351
|
+
|
|
352
|
+
| Component | Turns/calls | Tokens | Cost |
|
|
353
|
+
| --- | --- | --- | --- |
|
|
354
|
+
| Claude Code Worker | 70 turns | 15.5M cache-read + 370k cache-write + 100k output | $18.46 |
|
|
355
|
+
| Pi Decision Worker | 30 model calls | ~1.0M (91k uncached + 914k cache-read) | $0.04 |
|
|
356
|
+
|
|
357
|
+
Almost all of the money goes to the Worker, not the Supervisor's own Decision
|
|
358
|
+
Worker or Reviewer calls. In this run the Worker averaged ~220k tokens of
|
|
359
|
+
context per turn because it ran as a single long `-p` session under a
|
|
360
|
+
1M-token window that never compacted; a trivial Claude Code turn costs
|
|
361
|
+
roughly 24k prompt tokens for its system prompt alone, regardless of which
|
|
362
|
+
MCP servers are configured. Of the 30 Decision Worker calls, 28 were
|
|
363
|
+
permission requests, and the Decision Worker overrode the deterministic
|
|
364
|
+
policy 4 times (denying downloads and writes outside the task directory) —
|
|
365
|
+
this is why `hybrid` is the default `permissionAuthority`, not `policy`.
|
|
366
|
+
Replaying those 28 requests through the shipped `isRoutinePermission`
|
|
367
|
+
classifier answers 4 of them locally; that task was dominated by inline
|
|
368
|
+
`node -e` scripts and `$(...)` substitutions, which are never routine. An
|
|
369
|
+
ordinary implementation task is mostly in-cwd `Edit`/`Write`, `npm test` and
|
|
370
|
+
`git status/diff/add/commit`, all of which are routine, so its Decision
|
|
371
|
+
Worker call count drops much further.
|
|
372
|
+
|
|
373
|
+
Knobs, with their defaults and trade-offs:
|
|
374
|
+
|
|
375
|
+
- `PERMISSION_AUTHORITY` (`policy` | `hybrid` default | `decision-worker`):
|
|
376
|
+
`hybrid` answers routine in-cwd file edits and local read-only/dev shell
|
|
377
|
+
commands from the deterministic policy alone (`isRoutinePermission` in
|
|
378
|
+
`src/policy.ts`) and still sends every ambiguous request, and every policy
|
|
379
|
+
denial, to the Decision Worker. This mainly buys latency and a smaller
|
|
380
|
+
Decision Worker context, not dollars: the 30 calls above already cost
|
|
381
|
+
$0.04.
|
|
382
|
+
- `WORKER_MODEL` / `--model`: roughly a 5x price difference between Opus- and
|
|
383
|
+
Sonnet-class models. This is the single largest lever on the actual bill,
|
|
384
|
+
and it is the operator's choice; the Supervisor does not pick it for you.
|
|
385
|
+
- `WORKER_AUTOCOMPACT_TOKENS` (default 200000 in automatic mode; `0` keeps
|
|
386
|
+
Claude's own default): bounds context per Worker turn so a long session
|
|
387
|
+
does not keep accumulating ~220k-token turns. Worth tens of percent, at
|
|
388
|
+
the cost of some context quality.
|
|
389
|
+
- `WORKER_MAX_BUDGET_USD` / `autonomy.maxWorkerCostUsd`: a hard cap passed to
|
|
390
|
+
Claude as `--max-budget-usd` and re-checked by the Supervisor against the
|
|
391
|
+
cumulative Worker `result` cost. It is a cap, not a saving; a task that
|
|
392
|
+
hits it is parked with its evidence.
|
|
393
|
+
- `WORKER_MCP_CONFIG` (`--strict-mcp-config --mcp-config`): restricts the
|
|
394
|
+
Worker to only the listed MCP servers. It bounds what the Worker can
|
|
395
|
+
reach, not the ~24k-token fixed overhead of an ordinary turn.
|
|
396
|
+
- `DECISION_MODEL` / `REVIEWER_MODEL` (`provider/model-id`, for example
|
|
397
|
+
`anthropic/claude-haiku-4-5-20251001`): the Pi Decision Worker and
|
|
398
|
+
Reviewer models. Pi-side usage was already a few cents in this run, so a
|
|
399
|
+
cheaper model here mostly buys latency, not headline savings.
|
|
400
|
+
- `DECISION_COMPACT_TOKENS` (default 60000; `0` disables): proactively
|
|
401
|
+
compacts the persistent Decision Worker session once its estimated context
|
|
402
|
+
passes this threshold, and re-sends the startup instructions once on the
|
|
403
|
+
next prompt after compaction.
|
|
404
|
+
|
|
405
|
+
The Supervisor records what it spends rather than estimating it after the
|
|
406
|
+
fact: every Worker `result` record becomes a `worker_usage` event, every
|
|
407
|
+
Decision Worker/Reviewer model call becomes a `pi_usage` event, and both
|
|
408
|
+
accumulate into `session.usage` (`SupervisorTokenUsage`). `/supervise status
|
|
409
|
+
<task-id>` prints a `cost=… workerTurns=… workerTokens=… piTokens=…
|
|
410
|
+
decisionCalls=… reviewerCalls=…` summary; progress notifications carry
|
|
411
|
+
`SupervisorProgress.costUsd`/`.piTokens`, and a candidate notification
|
|
412
|
+
carries the same summary through `CandidateNotice.usage`, which the generic
|
|
413
|
+
webhook serialises as a numeric `usage` object and the WeCom format renders
|
|
414
|
+
as two extra lines.
|
|
415
|
+
|
|
416
|
+
None of this changes what a task actually costs beyond the Worker model and
|
|
417
|
+
budget choice; the Supervisor-side changes here mainly cut Decision Worker
|
|
418
|
+
tokens and latency, which were cents to begin with. For a cost-sensitive
|
|
419
|
+
unattended run, a reasonable starting point is a Sonnet-class `WORKER_MODEL`,
|
|
420
|
+
an explicit `WORKER_MAX_BUDGET_USD` per task, the default `hybrid` permission
|
|
421
|
+
authority, and a Haiku-class `DECISION_MODEL`.
|
|
270
422
|
|
|
271
423
|
## Development
|
|
272
424
|
|
|
425
|
+
Contributing to the extension itself (not required to use it):
|
|
426
|
+
|
|
273
427
|
```bash
|
|
274
|
-
npm
|
|
275
|
-
npm
|
|
276
|
-
npm run check:package
|
|
277
|
-
npm run check:docs
|
|
278
|
-
npm run check:automation
|
|
279
|
-
npm run check:workflows
|
|
428
|
+
npm ci --ignore-scripts
|
|
429
|
+
npm run check
|
|
280
430
|
npm run build
|
|
431
|
+
npm run test:pi
|
|
432
|
+
npm run test:install
|
|
281
433
|
```
|
|
282
434
|
|
|
283
|
-
|
|
284
|
-
|
|
435
|
+
Some tests validate the trusted-executable and protected-branch boundaries
|
|
436
|
+
against the real checkout, so they must run from a non-protected branch and
|
|
437
|
+
from a path that is not group/world-writable.
|
|
285
438
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
439
|
+
See [architecture](docs/architecture.md), [testing](docs/testing.md), and
|
|
440
|
+
[releasing](docs/releasing.md) for more detail; `docs/autonomy-target.md`
|
|
441
|
+
records the confirmed unattended-development target this project is built
|
|
442
|
+
around.
|
|
290
443
|
|
|
291
444
|
## License
|
|
292
445
|
|