pi-claude-supervisor 0.5.3 → 0.5.4

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented here.
4
4
 
5
+ ## [0.5.4](https://github.com/btnalit/pi-claude-supervisor/compare/v0.5.3...v0.5.4) (2026-09-15)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * harden unattended local lifecycle ([4f68760](https://github.com/btnalit/pi-claude-supervisor/commit/4f6876044cd8be00b0376239377b02b70aae8473))
11
+
5
12
  ## [0.5.3](https://github.com/btnalit/pi-claude-supervisor/compare/v0.5.2...v0.5.3) (2026-09-15)
6
13
 
7
14
 
@@ -43,10 +50,12 @@ All notable changes to this project will be documented here.
43
50
  - Replace shell-policy lexical bypasses with quote-aware parsing, nested-shell inspection and protected Git-ref checks; deny dynamic shell expansions while preserving literal argv values, and reject arbitrary non-Claude automatic executables.
44
51
  - Normalize malformed custom Reviewer values to blocking reports, persist recovery baselines and the resolved Claude executable identity, filter automatic environment variables with a deny-by-default allowlist, and compare the exact startup HEAD at the adapter spawn boundary.
45
52
  - Keep protected CI checks on the exact checked-out commit while giving automatic-mode fixtures a local validation branch and an owned deterministic Claude executable.
53
+ - Add a Supervisor-owned automatic tmux bridge that renders Claude stream-json in a live PTY and carries structured records through private framing instead of an independent event sidecar; adopted sessions remain manual-only.
54
+ - Add parent-identity tmux guardians, guarded Linux bootstrap readiness/cleanup, explicit nested-Agent/background-reviewer denial, and tolerant Reviewer prose/fence/repeated-JSON parsing with separate display-truncation markers.
46
55
 
47
- ### Remaining hardening gate
56
+ ### Release readiness
48
57
 
49
- - Open the protected pull request and complete independent CI/integration review; the exact-head read-only review, real editable Claude `2.1.270` repair/reacceptance drill and cleanup/lease evidence are recorded in `docs/automation-hardening-plan.md`.
58
+ - The exact-head read-only review, real editable Claude `2.1.270` repair/reacceptance drill and cleanup/lease evidence are recorded in `docs/automation-hardening-plan.md`. The review's pathname TOCTOU concern is explicitly accepted as a false positive for the trusted local-development threat model; host-side broker isolation remains future work for an untrusted-worker mode.
50
59
 
51
60
  ### Added
52
61
 
package/README.cn.md CHANGED
@@ -8,14 +8,14 @@
8
8
 
9
9
  用于 Pi 的 Claude Code Worker 监督扩展。MVP 中 Pi 负责生命周期、状态机、策略门和独立验收;Worker 只是被显式启动的子进程。
10
10
 
11
- > `v0.5.2` 已发布为单 Worker recovery 基线。默认手动 transport 是无额外依赖的
12
- > process pipe,不是 PTY;自动模式只使用 Claude JSONL,tmux 保留为手动交互。当前工作树已实现
11
+ > `v0.5.3` 已发布为单 Worker recovery 基线。默认手动 transport 是无额外依赖的
12
+ > process pipe,不是 PTY;自动模式支持 Claude JSONL 或 Supervisor 自有的 tmux bridge,被接管的
13
+ > tmux session 仍仅限手动交互。当前工作树已实现
13
14
  > repairable/persistent 能力拆分、可取消验收/Reviewer、证据完整性门禁、启动前
14
15
  > preflight 和阶段进度通知;真实 Claude Code `2.1.270` 允许编辑的
15
16
  > repair/reacceptance 演练已在隔离临时 worktree 通过。确认的产品目标是本地开发
16
17
  > 完全无人值守;详见 [自动化目标](docs/autonomy-target.md)。代码进入远程仓库或
17
- > main/integration 分支必须经过独立边界,Worker 不拥有 push/merge 权限;自动模式仅使用
18
- > JSONL,tmux 保留为手动交互 transport。
18
+ > main/integration 分支必须经过独立边界,Worker 不拥有 push/merge 权限。
19
19
  >
20
20
  > **无人值守状态:** 自动模式会自主完成本地修改、测试、有限修复、验收、独立 Review
21
21
  > 和本地提交检查;无法形成候选时自动挂起为不可发布候选。可选出站通知不授予权限,
@@ -55,9 +55,10 @@ export PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_FORMAT=generic
55
55
  # export PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_SECRET='shared-secret'
56
56
  ```
57
57
 
58
- 自动模式默认并且只能使用 `claude-jsonl`,通过 `result`、`control_request` 和进程
59
- `exit` 事件唤醒 Decision Worker;tmux 仅用于手动屏幕交互,不使用 JSONL 权限协议,也不会依赖 `/supervise poll` 轮询。本版本固定按已验证设备的
60
- Claude CLI `2.1.270` 运行,跨版本兼容性不在本轮范围内。
58
+ 自动模式支持 `claude-jsonl` 和 Supervisor 自有的 tmux bridge;JSONL 的 `result`、
59
+ `control_request` 和进程 `exit` 事件会唤醒 Decision Worker。tmux bridge 把结构化记录
60
+ 通过同一个 live PTY 的私有 terminal framing 传回适配器,不创建独立 JSONL sidecar;
61
+ `adopt-tmux` 仍是手动模式。本版本固定按已验证设备的 Claude CLI `2.1.270` 运行,跨版本兼容性不在本轮范围内。
61
62
 
62
63
  然后在 Pi 中使用:
63
64
 
@@ -114,8 +115,9 @@ Claude CLI `2.1.270` 运行,跨版本兼容性不在本轮范围内。
114
115
  自动模式会将 Decision Worker 会话持久化到状态目录。Pi 非正常重启后,`/supervise sessions`
115
116
  会显示 `recoverable` 任务;显式执行 `/supervise recover [--takeover] <task-id>` 会恢复 Decision Worker 上下文并
116
117
  重新启动 Claude Worker,不会静默恢复或重复执行任务。旧 Pi 进程已退出且租约确认旧 Worker
117
- 进程组已消失且 cgroup 仍是真实、可读取的空边界时,才可显式添加 `--takeover`;缺失、仍存活或无法确认的 Worker 会被拒绝。持久 tmux
118
- Worker 应使用 `adopt-tmux`,而不是 takeover。
118
+ 进程组已消失且 cgroup 仍是真实、可读取的空边界时,才可显式添加 `--takeover`;缺失、仍存活或无法确认的 Worker 会被拒绝。手动 owned tmux
119
+ Worker 可在重启后使用 `adopt-tmux`,而不是 takeover;自动 bridge 会由 parent-death guardian
120
+ 在 Supervisor 消失时终止,不提供自动 session 的重接管。
119
121
  自动模式下,Decision Worker 在任务授权范围内自动处理普通问题、测试失败和修复轮次,记录假设和证据;无法形成可交付候选时自动挂起并保留证据,而不是要求人工必须在线。可通过 `PI_CLAUDE_SUPERVISOR_REQUIRE_LOCAL_COMMIT=0` 或 task `autonomy.requireLocalCommit` 关闭本地 commit 要求,但自动模式仍要求有效 Git baseline 和非保护 worktree;远程 push 和 main/integration merge 仍由独立边界控制。
120
122
 
121
123
  `v0.5.0` 已完成并发布“多命令验收—独立只读 Reviewer—结构化修复轮次—再次验收”闭环。
@@ -124,7 +126,7 @@ Worker 应使用 `adopt-tmux`,而不是 takeover。
124
126
  Reviewer 只能使用 `read`、`grep`、`find`、`ls`,不会修改工作树或批准权限。自动模式在
125
127
  Worker 启动前捕获 git baseline,要求完整的 baseline-relative tracked/commit/untracked evidence,
126
128
  并在默认情况下要求 Worker 在非保护分支本地 commit;无效输出、证据不完整、重复 finding、P0/P1 或预算耗尽
127
- 会自动挂起候选。自动模式拒绝 process-pipe 和 tmux,并在模型执行前检查目录、可执行文件、依赖
129
+ 会自动挂起候选。自动模式拒绝 process-pipe,并在模型执行前检查目录、可执行文件、依赖
128
130
  和 cgroup;Worker 环境会过滤远程仓库凭据并禁用 Git 全局凭据 helper。详见 [自动化目标](docs/autonomy-target.md)。协同多 Worker 属于后续独立开发阶段,
129
131
  自动模式只接受裸的直接 Claude 命令名,会固定解析后的操作者拥有的可执行文件,并请求 fail-closed 的 Claude Code Bash sandbox;任意自定义可执行文件和显式可执行路径会在自动模式拒绝。需要固定路径时设置 `PI_CLAUDE_SUPERVISOR_TRUSTED_CLAUDE`。手动/自定义 Worker 的完整 host-level sandbox 仍需由集成方提供。
130
132
 
@@ -134,8 +136,10 @@ Worker 启动前捕获 git baseline,要求完整的 baseline-relative tracked/
134
136
 
135
137
  ```bash
136
138
  export PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux
137
- export PI_CLAUDE_SUPERVISOR_WORKER='claude --permission-mode plan'
138
- # tmux 仅为手动交互;无人值守 Decision Worker 必须使用 JSONL。
139
+ export PI_CLAUDE_SUPERVISOR_WORKER='claude'
140
+ # 当前手动 tmux 也要求 Linux(用于 pane identity 和清理);可使用 cgroup auto/off。
141
+ # 设置 PI_CLAUDE_SUPERVISOR_MODE=auto 启用 Supervisor 自有的自动 bridge;自动 tmux
142
+ # 要求 Linux cgroup v2 和 parent-death guardian,缺失时会 fail closed。
139
143
  # 接管非默认 tmux server 时可选:
140
144
  # export PI_CLAUDE_SUPERVISOR_TMUX_SOCKET=/path/to/tmux.sock
141
145
  ```
@@ -143,8 +147,12 @@ export PI_CLAUDE_SUPERVISOR_WORKER='claude --permission-mode plan'
143
147
  `/supervise start <task>` 会在私有 tmux server 中启动 Claude,并返回可复制的 attach 命令。
144
148
  可以在另一个终端 attach 到同一个 PTY,观察或人工输入。多行消息通过 tmux buffer 和 Enter
145
149
  发送,不会把消息拼接进 shell 命令;`pipe-pane` 记录原始输出,`capture-pane` 检测稳定的 Claude
146
- 输入提示,并复用 watchdog、审计和独立验收流程;由于 tmux 没有结构化权限边界,
147
- 不会在 tmux 中启用自动 Decision Worker 或无人值守远程边界策略。
150
+ 输入提示,并复用 watchdog、审计和独立验收流程。自动模式会在 pane 内启动 bridge:它运行
151
+ Claude stream-json、把可读输出渲染到附着的终端,并通过同一 PTY 的私有 framing 返回结构化
152
+ 记录;bridge 及其后代放入受 Supervisor 管理的 Linux cgroup,并由 parent-death guardian
153
+ 保护;任一 containment 机制不可用时 fail closed。适配器直接从 PTY 原始 pipe 解析,因此没有
154
+ 独立 JSONL sidecar。自动模式拒绝被接管的 session;Supervisor 自有 bridge 支持自动输入串行化、
155
+ 权限响应、turn 完成和 stop。
148
156
 
149
157
  如果 Claude 已由你在 tmux 中启动,可以显式接管且不会重放原始任务:
150
158
 
@@ -159,7 +167,8 @@ export PI_CLAUDE_SUPERVISOR_WORKER='claude --permission-mode plan'
159
167
  `tmux kill-session`。`/supervise takeover <task-id>` 会暂停 Decision Worker 自动发送,只有
160
168
  `/supervise resume-auto <task-id>` 才恢复。
161
169
 
162
- PTY 屏幕文字不是 Claude JSONL,不能把屏幕文字当作结构化权限证据。TUI 决策应按任务授权策略处理并记录;无法形成候选时可以自动挂起,不要求人工持续在线。普通终端里已经运行的 Claude 不能安全迁移进 tmux;`--resume`
170
+ PTY 屏幕文字本身不是 Claude JSONL,不能把屏幕文字当作结构化权限证据;只有 Supervisor bridge
171
+ 的私有 framing 记录才是结构化证据。TUI 决策应按任务授权策略处理并记录;无法形成候选时可以自动挂起,不要求人工持续在线。普通终端里已经运行的 Claude 不能安全迁移进 tmux;`--resume`
163
172
  是读取历史的新进程,不是实时 attach。实时测试请使用 plan/read-only 参数。
164
173
 
165
174
  可以从不同工作目录启动多个任务会话;活动会话不能共享同一 cwd,建议每个任务使用独立 worktree:
package/README.md CHANGED
@@ -10,10 +10,10 @@ A policy-gated [Pi](https://pi.dev) extension for supervising a Claude Code work
10
10
  The MVP keeps Pi in control of lifecycle, state, policy and verification while the
11
11
  worker remains an explicitly started child process.
12
12
 
13
- > **Release status:** `v0.5.2` is the released single-Worker recovery baseline. The
13
+ > **Release status:** `v0.5.3` is the released single-Worker recovery baseline. The
14
14
  > default manual transport is dependency-free process pipes, not PTY. Automatic
15
- > supervision uses Claude JSONL only; tmux is manual-only because it has no structured
16
- > permission boundary. Repairable-vs-persistent capabilities,
15
+ > supervision uses Claude JSONL or the Supervisor-owned tmux bridge; adopted tmux
16
+ > sessions remain manual-only. Repairable-vs-persistent capabilities,
17
17
  > cancellable verification, evidence completeness gates, startup preflight and phase
18
18
  > progress reporting. A real edit-capable Claude Code `2.1.270` repair/reacceptance
19
19
  > drill passed in an isolated temporary worktree. The confirmed product target is
@@ -148,6 +148,10 @@ candidate from crossing the remote/main boundary. Automatic mode rejects explici
148
148
  `process-pipe` and preflights runtime prerequisites. Invalid output, unavailable
149
149
  evidence, duplicate findings, P0/P1 findings and exhausted repair budgets park the
150
150
  candidate without waiting for a human; see [the autonomy target](docs/autonomy-target.md).
151
+ The automatic tmux bridge carries Claude stream-json records inside the same live PTY
152
+ as display output using private terminal framing; it does not create an independent
153
+ structured-event sidecar. Automatic tmux accepts only Supervisor-owned sessions,
154
+ while `adopt-tmux` remains manual-only.
151
155
  Coordinated multi-worker scheduling is a later milestone; CI uses deterministic fake
152
156
  Workers/replay fixtures, and real multi-worker Claude tests remain authenticated
153
157
  manual Spikes. Full host-level sandboxing and low-privilege execution for custom
@@ -175,9 +179,11 @@ For an interactive Claude Code window, opt in to the tmux transport:
175
179
 
176
180
  ```bash
177
181
  export PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux
178
- export PI_CLAUDE_SUPERVISOR_WORKER='claude --permission-mode plan'
179
- # tmux does not support cgroup required mode; use cgroup mode auto/off.
180
- # tmux is manual-only; automatic Decision Worker supervision requires JSONL.
182
+ export PI_CLAUDE_SUPERVISOR_WORKER='claude'
183
+ # Manual tmux currently requires Linux for process identity and cleanup;
184
+ # it may use cgroup mode auto/off.
185
+ # Set PI_CLAUDE_SUPERVISOR_MODE=auto for the Supervisor-owned automatic bridge;
186
+ # automatic tmux additionally requires Linux cgroup v2 and a parent-death guardian.
181
187
  # Optional, only when adopting a non-default tmux server:
182
188
  # export PI_CLAUDE_SUPERVISOR_TMUX_SOCKET=/path/to/tmux.sock
183
189
  ```
@@ -187,10 +193,14 @@ literal attach command. Use that command in another terminal to watch or
187
193
  manually interact with the same PTY; attaching is optional for unattended local
188
194
  development. The adapter sends multi-line input through
189
195
  tmux buffers and Enter, never by interpolating the message into a shell command.
190
- It records the PTY stream with `pipe-pane` and uses `capture-pane` to detect a
191
- stable Claude input prompt. Because tmux has no structured permission boundary,
192
- automatic Decision Worker supervision is disabled for this transport; use JSONL for
193
- unattended decisions, repair and protected command enforcement.
196
+ In automatic mode the Supervisor starts a bridge in the pane: it runs Claude's
197
+ stream-json protocol inside the live PTY, contains the bridge and descendants in
198
+ an owned cgroup, fails closed when that containment or the Linux guardian is
199
+ unavailable, renders readable deltas for the attached terminal, and returns
200
+ structured records through private terminal framing on the same PTY.
201
+ The adapter parses those records from the raw PTY pipe, so there is no independent
202
+ JSONL event sidecar. Automatic mode refuses adopted sessions; use JSONL or the owned
203
+ bridge for unattended decisions, repair and protected command enforcement.
194
204
 
195
205
  A session that you started yourself can be explicitly adopted without replaying
196
206
  the task:
@@ -211,13 +221,15 @@ kill-session` yourself when the adopted window should be closed.
211
221
  `/supervise takeover <task-id>` disables automatic Decision Worker messages;
212
222
  resume them only with `/supervise resume-auto <task-id>`.
213
223
 
214
- PTY screen text is not Claude JSONL and must not be treated as structured
215
- permission evidence. TUI decisions follow the configured autonomy policy and are
224
+ PTY screen text is not itself Claude JSONL and must not be treated as structured
225
+ permission evidence; only the Supervisor bridge's private framed records are
226
+ authoritative. TUI decisions follow the configured autonomy policy and are
216
227
  recorded; an unresolved task may be parked without requiring a human to remain
217
228
  online. A normal terminal Claude process cannot be migrated into tmux, and
218
- `--resume` is historical recovery rather than live PTY attach. Owned tmux sessions
219
- survive a Pi disconnect and require an explicit `adopt-tmux` after restart. Use
220
- plan/read-only flags for live testing.
229
+ `--resume` is historical recovery rather than live PTY attach. Manual owned tmux
230
+ sessions survive a Pi disconnect and require an explicit `adopt-tmux` after
231
+ restart; automatic owned sessions are terminated by their parent-death guardian
232
+ when the Supervisor disappears. Use plan/read-only flags for live testing.
221
233
 
222
234
  ## Development
223
235
 
@@ -77,24 +77,25 @@ one capability from the other.
77
77
 
78
78
  This is a control-boundary fixture and headless transport. It does not emulate a
79
79
  terminal. Manual compatibility mode remains `process-pipe`; automatic mode
80
- (`PI_CLAUDE_SUPERVISOR_MODE=auto`) defaults to Claude JSONL and uses the CLI
81
- contract validated by the fixed-version spike; an explicit tmux transport remains
82
- screen-based.
80
+ (`PI_CLAUDE_SUPERVISOR_MODE=auto`) defaults to Claude JSONL and can select the
81
+ Supervisor-owned tmux bridge. The bridge uses the CLI contract validated by the
82
+ fixed-version spike, renders the stream in the pane and carries structured records
83
+ through private framing on the same PTY; explicit adoption remains manual-only.
83
84
 
84
85
  A worker exit automatically triggers cleanup, and terminal status waits for
85
- that cleanup to be confirmed (or reports a cleanup error). On Linux the adapter
86
- uses cgroup v2 automatically when the current user cgroup is writable; the
87
- `required` mode performs a preflight and fails before Claude starts if cgroup
88
- attachment or cleanup is unavailable. Cgroup
89
- cleanup kills descendants even when they call `setsid()` or create another
90
- process group. Attachment occurs immediately after spawn, so a worker that
91
- forks before attachment remains a documented startup-window limitation.
92
-
93
- When cgroup v2 is unavailable, the adapter falls back to detached
94
- process-group cleanup. That fallback is not recursive: `setsid()` descendants
95
- can escape, and PID reuse between leader exit and cleanup is a host-level
96
- limitation. Production deployments that require an atomic boundary should use a
97
- service-manager scope, Job Object, pidfd-aware reaper, or equivalent supervisor.
86
+ that cleanup to be confirmed (or reports a cleanup error). On Linux, manual
87
+ workers may use cgroup v2 automatically when the current user cgroup is writable;
88
+ the `required` mode performs a preflight and fails before Claude starts if cgroup
89
+ attachment or cleanup is unavailable. Automatic JSONL workers always require the
90
+ same preflight and a guarded cgroup bootstrap; automatic startup fails closed on
91
+ non-Linux hosts or when the boundary cannot be established. Cgroup cleanup kills
92
+ descendants even when they call `setsid()` or create another process group.
93
+
94
+ When cgroup v2 is unavailable, manual mode falls back to detached process-group
95
+ cleanup. That fallback is not recursive: `setsid()` descendants can escape, and
96
+ PID reuse between leader exit and cleanup is a host-level limitation. Production
97
+ deployments that require an atomic boundary should use a service-manager scope,
98
+ Job Object, pidfd-aware reaper, or equivalent supervisor.
98
99
  Pi owns graceful `SIGTERM`/`SIGINT` handling and invokes the extension's
99
100
  `session_shutdown` hook. The extension does not install a second `process.exit()`
100
101
  handler, avoiding races with Pi terminal restoration and other extensions. `SIGSTOP`
@@ -104,25 +105,37 @@ host-fatal signals.
104
105
  ## tmux/PTY transport
105
106
 
106
107
  `TmuxWorkerAdapter` is an explicit second transport, selected with
107
- `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux`. Because tmux has no equivalent cgroup
108
- containment boundary, `PI_CLAUDE_SUPERVISOR_CGROUP_MODE=required` is rejected
109
- with this transport; use `auto`/`off` only when the tmux boundary is acceptable.
110
- An owned worker gets a private tmux server/socket and executes the validated Claude command directly in the pane,
111
- so its pane identity remains re-adoptable after a Pi restart. The worker
108
+ `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux`. Both owned and adopted tmux currently
109
+ require Linux because pane identity and cleanup use `/proc`; manual tmux may use
110
+ cgroup `auto`/`off`. Automatic owned tmux additionally creates a required Linux
111
+ cgroup for the bridge and its descendants; it is rejected when cgroup v2 or the
112
+ parent-death guardian is unavailable.
113
+ An owned manual worker gets a private tmux server/socket and executes the
114
+ validated Claude command directly in the pane. An owned automatic worker instead
115
+ starts the Supervisor bridge through a cgroup-joining pane bootstrap, so its
116
+ bridge identity is not a manual adoption target. The worker
112
117
  environment is supplied to the tmux server through the same least-privilege
113
118
  environment builder; credentials are not copied into a file; credential-shaped
114
119
  command arguments are rejected.
115
120
  `load-buffer`, bracketed `paste-buffer` and `send-keys Enter` provide the input
116
121
  boundary without interpolating a task into a shell command. C0/C1 terminal
117
- control bytes are rejected; CRLF is normalized to a newline.
122
+ control bytes are rejected; CRLF is normalized to a newline. In automatic mode,
123
+ the adapter snapshots the trusted direct Claude process and checks both the
124
+ required Linux cgroup and process tree on every poll; a newly executed Claude or
125
+ Reviewer descendant is a runtime policy failure and the owned session is stopped.
126
+ This supplements the lexical Bash/file-tool boundary and is disabled for
127
+ manual/adopted sessions.
118
128
 
119
129
  The transport has three deliberately separate observations:
120
130
 
121
131
  - `pipe-pane` provides an append-only raw PTY log for output polling and audit;
122
132
  - `capture-pane` provides a bounded screen snapshot used only for stable prompt
123
133
  detection and human display;
124
- - Claude's own transcript, when available, remains the structured history. The
125
- screen is never relabeled as JSONL or permission evidence.
134
+ - the Supervisor-owned bridge emits Claude stream-json records as private framed
135
+ PTY control data; `pipe-pane` carries those records to the adapter without an
136
+ independent JSONL sidecar;
137
+ - Claude's own transcript, when available, remains the structured history. Ordinary
138
+ screen text is never relabeled as JSONL or permission evidence.
126
139
 
127
140
  For an owned initial turn, the adapter emits a synthetic `turn_completed` only
128
141
  after output activity and two stable input-prompt observations. Adopting an idle
@@ -139,9 +152,12 @@ process identity before attaching. Every later input, capture and signal uses
139
152
  that immutable pane target; a replacement process is refused. Adopted sessions
140
153
  are not owned: stop and Pi shutdown detach rather than kill them. Tmux commands
141
154
  and serialized input waits have bounded deadlines so shutdown cannot hang
142
- forever. Sessions started by the adapter also survive a Pi disconnect, but
143
- recovery after restart is explicit re-adoption; the extension never claims to
144
- attach to an arbitrary non-tmux PTY. A normal Claude
155
+ forever. Manual sessions started by the adapter survive a Pi disconnect, but recovery
156
+ after restart is explicit re-adoption; automatic sessions are intentionally
157
+ terminated by their parent-death guardian when the Supervisor disappears. The
158
+ extension never claims to attach to an arbitrary non-tmux PTY. Startup cleanup always attempts the
159
+ private tmux server teardown, including after partial session creation, and a
160
+ confirmed `kill-server` is sufficient cleanup evidence. A normal Claude
145
161
  `--resume` starts another process from history and is not a live PTY migration.
146
162
 
147
163
  ## State machine
@@ -144,7 +144,8 @@ diff: (none)
144
144
  - Reviewer evidence 使用任务 baseline-relative diff、baseline 后 commit summaries、受限 untracked regular-file 内容、路径组件/symlink 门禁,并对 incomplete/truncated fail-closed。
145
145
  - Acceptance 子进程、证据收集和 Reviewer 共享 abort signal;Pi UI 可看到 startup、Worker heartbeat、acceptance、review、repair 和 candidate/decision phases。
146
146
  - 自动模式启动前检查运行目录、cwd、Worker 可执行文件、transport 依赖和 required cgroup;显式
147
- `process-pipe` 和 `tmux` 不再进入自动模式,后者保留为手动 PTY transport。
147
+ `process-pipe` 仍仅限手动模式,Supervisor 自有 tmux bridge 可进入自动模式,被接管的
148
+ session 仍仅限手动 PTY transport。
148
149
  - Reviewer/Decision Worker 只解析 assistant message 边界的最终文本;permission pending 会在自动响应后清除,候选状态不会被错误解除;扩大的 exec buffer 避免普通大测试报告被误判为命令失败。
149
150
 
150
151
  本轮已完成:
@@ -28,13 +28,15 @@ complete audit trail. These are reliability and containment mechanisms, not requ
28
28
  to approve every development action. Automatic mode also records and revalidates a full existing
29
29
  repository baseline, captures the startup HEAD and requires that exact HEAD again at
30
30
  final pre-spawn, requires a non-protected branch and a pinned operator-owned direct Claude
31
- JSONL Worker, and by default requires a local commit before a candidate is deliverable.
31
+ JSONL Worker or Supervisor-owned tmux bridge, and by default requires a local commit before a candidate is deliverable.
32
32
 
33
33
  ## 2. Hard authority boundary
34
34
 
35
- Automatic Worker supervision uses the structured JSONL transport; the interactive tmux transport
36
- remains manual-only because it has no equivalent permission-response boundary. The Worker and local
37
- automation do **not** receive authority or credentials for:
35
+ Automatic Worker supervision uses either the structured JSONL transport or a
36
+ Supervisor-owned tmux bridge. The bridge runs Claude's stream-json protocol inside the live
37
+ PTY, renders a human-readable display, and returns private framed records through the same
38
+ PTY; adopted tmux sessions remain manual-only. The Worker and local automation do **not**
39
+ receive authority or credentials for:
38
40
 
39
41
  - pushing code to a remote repository;
40
42
  - merging into `main` or another protected integration branch;
@@ -100,8 +102,9 @@ Automatic mode implements the local loop: policy decisions allow ordinary local
100
102
  continue/redirect/answer/repair, acceptance and independent Review run without a human callback,
101
103
  and unresolved situations become `blocked` candidates. The default task autonomy is unattended, requires a local commit, and permits two bounded
102
104
  Decision Worker request retries. Automatic startup rejects non-Git/detached/bare/protected
103
- repository states, malformed baselines, startup-HEAD races, non-JSONL transports and
104
- non-Claude or untrusted executable identities before Worker startup. The resolved
105
+ repository states, malformed baselines, startup-HEAD races, the unstructured
106
+ process-pipe transport and non-Claude or untrusted executable identities before Worker
107
+ startup. The resolved
105
108
  executable identity is persisted with the Decision Worker recovery record and must match
106
109
  again during recovery.
107
110
 
@@ -36,7 +36,7 @@ Claude Code Worker
36
36
  4. Supervisor 因误判导致无限循环、危险操作或不可审计的修改。
37
37
  5. 人工无法随时接管或恢复任务。
38
38
 
39
- **当前状态:`v0.5.2` 已正式发布,已完成固定 Claude Code `2.1.270` 稳定性验证、单 Worker recovery、真实只读 Review drill、隔离临时 worktree 的允许编辑 repair/reacceptance drill、exact-head 独立 Review 和受保护发布。当前工作树已落实 repairable/persistent 能力拆分、verifying stop、paused watchdog、baseline-relative repository evidence、可取消验收/Reviewer、启动 preflight、无人值守权限决策、local-commit enforcement、候选挂起和阶段进度通知。自动模式现在要求 direct Claude JSONL、完整 Git baseline、非保护分支,并请求不可用即失败的 Claude Code sandbox 和无出站域名;任意自定义可执行文件不会进入自动模式。Legacy human/takeover APIs 仅保留显式兼容控制;普通不确定性不再阻塞本地循环。协同多 Worker、手动/自定义集成的 host-level 低权限和网络隔离仍是独立后续里程碑。**
39
+ **当前状态:`v0.5.3` 已正式发布,已完成固定 Claude Code `2.1.270` 稳定性验证、单 Worker recovery、真实只读 Review drill、隔离临时 worktree 的允许编辑 repair/reacceptance drill、exact-head 独立 Review 和受保护发布。当前工作树已落实 repairable/persistent 能力拆分、verifying stop、paused watchdog、baseline-relative repository evidence、可取消验收/Reviewer、启动 preflight、无人值守权限决策、local-commit enforcement、候选挂起和阶段进度通知。自动模式现在要求 direct Claude JSONL 或 Supervisor 自有 tmux bridge、完整 Git baseline、非保护分支,并请求不可用即失败的 Claude Code sandbox 和无出站域名;任意自定义可执行文件不会进入自动模式。Legacy human/takeover APIs 仅保留显式兼容控制;普通不确定性不再阻塞本地循环。协同多 Worker、手动/自定义集成的 host-level 低权限和网络隔离仍是独立后续里程碑。**
40
40
 
41
41
  ---
42
42
 
@@ -39,6 +39,14 @@ boundary.
39
39
 
40
40
  ## Residual risks and follow-up hardening
41
41
 
42
+ The final independent review also raised a pathname TOCTOU concern for Claude
43
+ file-tool authorization. That finding is intentionally recorded as a false positive
44
+ for this release's trusted local-development threat model: normal edits may replace
45
+ file contents, but automatic workers are not treated as hostile same-UID filesystem
46
+ actors, and this policy is a metadata guard rather than a host filesystem isolation
47
+ boundary. A future untrusted-worker mode would need a host-side broker or an OS
48
+ sandbox that prevents `.git` writes.
49
+
42
50
  These are verified limitations and follow-up work after the automatic boundary
43
51
  hardening:
44
52
 
package/docs/testing.md CHANGED
@@ -57,7 +57,9 @@ uses Claude Code 2.1.270 at
57
57
  `/home/yancao/.local/share/mise/installs/claude/2.1.270/claude`, including real
58
58
  owned tmux turns, pause/resume, and restart re-adoption.
59
59
  The adapter regression suite also verifies event subscription, parsed
60
- `permission_request` events, and the exact nested `control_response` envelope.
60
+ `permission_request` events, the exact nested `control_response` envelope, and
61
+ that an automatic tmux Worker cannot launch a second Claude executable from a
62
+ Worker-created script.
61
63
  The automation spike additionally exercises a real Pi SDK Decision Worker with
62
64
  Claude: ordinary completion, harmless Bash permission handling, and an
63
65
  `AskUserQuestion` denial-to-text fallback followed by automatic verification.
@@ -118,19 +120,24 @@ relevant checks from a clean host perspective.
118
120
 
119
121
  Automatic mode is enabled with `PI_CLAUDE_SUPERVISOR_MODE=auto`; it defaults to
120
122
  JSONL and routes `result`, permission, and process-exit events to the persistent
121
- Pi Decision Worker. Task autonomy defaults to unattended local work, a required
123
+ Pi Decision Worker. Setting `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux` selects the
124
+ Supervisor-owned live bridge, which carries the same structured records through
125
+ private framing on the PTY rather than an independent JSONL sidecar. Task autonomy
126
+ defaults to unattended local work, a required
122
127
  local commit on a non-protected task branch and two bounded Decision Worker retries. Configure
123
128
  `PI_CLAUDE_SUPERVISOR_REQUIRE_LOCAL_COMMIT=0` or task `autonomy.requireLocalCommit`
124
129
  only to disable the local-commit deliverability check; automatic mode still requires a Git
125
- baseline and non-protected worktree. An explicit `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux` selection
126
- remains screen-based and does not use the JSONL permission protocol. `process-pipe` remains the manual compatibility mode. Candidate/failure notification is optional and outbound-only through
130
+ baseline and non-protected worktree. Adopted tmux sessions remain manual-only; the automatic bridge requires
131
+ Supervisor ownership. `process-pipe` remains the manual compatibility mode. Candidate/failure notification is optional and outbound-only through
127
132
  `PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_URL`; it is not a synchronous approval
128
133
  callback. Approval callbacks are deliberately not accepted without a separately
129
134
  authenticated endpoint.
130
135
 
131
136
  The tmux transport is selected with `PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux`.
132
- Automatic mode rejects explicit `process-pipe` and `tmux` transports; use JSONL for
133
- bounded decisions and repair. Built-in Claude workers also receive a fail-closed
137
+ Automatic mode rejects explicit `process-pipe`; use JSONL or the Supervisor-owned
138
+ bridge for bounded decisions and repair. Automatic JSONL and tmux workers also
139
+ require Linux cgroup v2 containment (and the tmux parent-death guardian); startup
140
+ fails closed when it is unavailable. Built-in Claude workers also receive a fail-closed
134
141
  sandbox setting (`failIfUnavailable`, `allowUnsandboxedCommands=false`, no outbound
135
142
  network domains); verify that startup fails if the sandbox cannot be initialized.
136
143
  Automatic startup also requires a full existing Git baseline, non-bare non-protected
@@ -141,7 +148,7 @@ compares the exact startup HEAD again immediately before spawn. Before release,
141
148
  private-socket attach,
142
149
  multi-line paste, prompt stability while
143
150
  Claude is busy, trust/permission policy handling, duplicate send prevention, pane
144
- replacement refusal, pause/resume, owned-session stop, adopted-session
151
+ replacement refusal, live bridge event framing, pause/resume, owned-session stop, adopted-session
145
152
  detach/re-adoption, bounded shutdown, and Pi shutdown without closing an
146
153
  attached window. The two gated real-Claude spikes above cover the trust prompt,
147
154
  permission prompt, exact output, optional takeover, and adopted detach paths. Use
@@ -156,7 +163,8 @@ response.
156
163
  The automated adapter matrix covers external `SIGTERM`, `SIGINT`, `SIGKILL`,
157
164
  `SIGSTOP`/`SIGCONT`, SIGTERM refusal/escalation, leader-early-exit descendant
158
165
  cleanup, required cgroup bootstrap containment of a pre-attachment detached
159
- and `setsid()` descendant, repeated stop, spawn failure, output truncation,
166
+ and `setsid()` descendant, automatic nested-Claude detection across the worker
167
+ cgroup, repeated stop, spawn failure, output truncation,
160
168
  blocked stdin write timeouts, and immediate JSONL results. The Supervisor
161
169
  matrix also covers retrying failed lifecycle events, preserving startup event
162
170
  order, stopping under persistent timeout-event failure, and restoring output
@@ -185,8 +193,12 @@ isolated temporary worktree. The current hardening plan and evidence paths are r
185
193
  in [`docs/automation-hardening-plan.md`](automation-hardening-plan.md). Deterministic
186
194
  coverage now includes repairable-vs-persistent capability assertions, cancellation
187
195
  of acceptance commands, stop-from-verifying precedence, paused watchdog baselining,
188
- staged/untracked evidence and untracked symlink rejection. The remaining release gate
189
- is the exact-head independent read-only review.
196
+ staged/untracked evidence, and untracked symlink/hard-link rejection. The exact-head
197
+ independent read-only review was rerun. Its pathname TOCTOU finding is recorded as a
198
+ false positive for the trusted local-development threat model: automatic workers are
199
+ trusted development agents, and this policy is a metadata guard rather than a host
200
+ filesystem isolation boundary. A hostile same-UID worker would require a separate
201
+ sandbox/broker design and is out of scope for this release.
190
202
 
191
203
  ### Acceptance and Reviewer fixtures
192
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-supervisor",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "A policy-gated Pi supervisor for observing and verifying Claude Code workers.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -168,9 +168,16 @@ export class DecisionSessionStore {
168
168
  const record = await this.#loadUnlocked(taskId);
169
169
  if (!record || record.state !== "active") throw new Error(`Decision Worker recovery record is unavailable: ${taskId}`);
170
170
  if (record.recoveryState === "ready" || record.recoveryState === "interrupted") return;
171
- if (!record.recoveryOwnerPid || await processExists(record.recoveryOwnerPid)) {
171
+ if (!record.recoveryOwnerPid || !record.recoveryOwnerStartTime) {
172
+ throw new Error(`Decision Worker recovery owner identity is unavailable: ${taskId}`);
173
+ }
174
+ const currentOwnerStartTime = await processStartTime(record.recoveryOwnerPid);
175
+ if (currentOwnerStartTime === record.recoveryOwnerStartTime) {
172
176
  throw new Error(`Decision Worker recovery owner is still live: ${taskId}`);
173
177
  }
178
+ if (!currentOwnerStartTime && await processExists(record.recoveryOwnerPid)) {
179
+ throw new Error(`Decision Worker recovery owner identity is unavailable: ${taskId}`);
180
+ }
174
181
  await this.#saveUnlocked({
175
182
  ...record,
176
183
  recoveryState: "interrupted",
package/src/index.ts CHANGED
@@ -39,19 +39,19 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
39
39
  if (!["off", "auto", "required"].includes(cgroupMode)) {
40
40
  throw new Error(`Unsupported PI_CLAUDE_SUPERVISOR_CGROUP_MODE: ${cgroupMode}; expected off, auto, or required`);
41
41
  }
42
- if (transport === "tmux" && cgroupMode === "required") {
43
- throw new Error("PI_CLAUDE_SUPERVISOR_CGROUP_MODE=required is unsupported with tmux; use process-pipe/jsonl or set cgroup mode to auto/off");
42
+ if (transport === "tmux" && cgroupMode === "required" && !automation) {
43
+ throw new Error("PI_CLAUDE_SUPERVISOR_CGROUP_MODE=required is unsupported with manual tmux; use automatic mode or cgroup mode auto/off");
44
44
  }
45
- if (automation && transport !== "jsonl") {
46
- throw new Error("automatic supervision requires PI_CLAUDE_SUPERVISOR_TRANSPORT=jsonl; process-pipe and tmux are manual-only");
45
+ if (automation && !["jsonl", "tmux"].includes(transport)) {
46
+ throw new Error("automatic supervision requires PI_CLAUDE_SUPERVISOR_TRANSPORT=jsonl or tmux; process-pipe is manual-only");
47
47
  }
48
48
  const adapter = transport === "tmux"
49
- ? new TmuxWorkerAdapter({ stateDir })
49
+ ? new TmuxWorkerAdapter({ stateDir, cgroupMode: automation ? "required" : cgroupMode as "off" | "auto" | "required" })
50
50
  : new ProcessWorkerAdapter({
51
51
  // Automatic decisions require Claude's structured event stream. The pipe
52
52
  // transport remains available for manual/compatibility sessions.
53
53
  mode: automation || transport === "jsonl" ? "claude-jsonl" : "process-pipe",
54
- cgroupMode: cgroupMode as "off" | "auto" | "required",
54
+ cgroupMode: automation ? "required" : cgroupMode as "off" | "auto" | "required",
55
55
  });
56
56
  const humanWebhook = new HumanWebhookNotifier({
57
57
  url: process.env.PI_CLAUDE_SUPERVISOR_HUMAN_WEBHOOK_URL,
@@ -232,7 +232,10 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
232
232
  const fileSpec = specPath ? await readTaskSpecFile(specPath, ctx.cwd) : undefined;
233
233
  const spec = fileSpec ?? { autonomy: autonomyDefaults() };
234
234
  const goal = fileSpec?.goal ?? task;
235
- const taskAutomation = automation && spec.autonomy.unattended;
235
+ // Adopted sessions are explicit manual compatibility controls. They
236
+ // never enter the automatic Reviewer/decision loop, even when the
237
+ // extension is globally configured for unattended starts.
238
+ const taskAutomation = operation !== "adopt-tmux" && automation && spec.autonomy.unattended;
236
239
  if (!goal) throw new Error(operation === "adopt-tmux" ? "Usage: /supervise adopt-tmux [--spec <file>] <tmux-session> <task>" : "Usage: /supervise start [--spec <file>] <task>");
237
240
  if (operation === "adopt-tmux" && adapter.capabilities().transport !== "tmux") throw new Error("/supervise adopt-tmux requires PI_CLAUDE_SUPERVISOR_TRANSPORT=tmux");
238
241
  const [command, ...workerArgs] = parseCommand(process.env.PI_CLAUDE_SUPERVISOR_WORKER ?? "claude");
@@ -259,7 +262,7 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
259
262
  }
260
263
  pendingCwds.add(cwdKey);
261
264
  const session = new Supervisor(adapter, events, {
262
- reviewer,
265
+ reviewer: taskAutomation ? reviewer : undefined,
263
266
  onCandidate: async (notice) => {
264
267
  if (ctx.hasUI) notify(ctx, `Candidate ${notice.status}: ${notice.reason}`, notice.status === "ready" ? "info" : "warning");
265
268
  if (humanWebhook.enabled) {
@@ -475,7 +478,7 @@ export default function piClaudeSupervisor(pi: ExtensionAPI): void {
475
478
  }
476
479
  pendingCwds.add(cwdKey);
477
480
  const session = new Supervisor(adapter, events, {
478
- reviewer,
481
+ reviewer: automaticRecovery ? reviewer : undefined,
479
482
  onCandidate: async (notice) => {
480
483
  if (ctx.hasUI) notify(ctx, `Candidate ${notice.status}: ${notice.reason}`, notice.status === "ready" ? "info" : "warning");
481
484
  if (humanWebhook.enabled) {