pi-claude-supervisor 0.5.0 → 0.5.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/CHANGELOG.md +11 -1
- package/README.cn.md +14 -6
- package/README.md +22 -10
- package/docs/architecture.md +30 -4
- package/docs/engineering-plan.md +54 -3
- package/docs/recovery-review.md +24 -0
- package/docs/stability-matrix-2.1.270.md +32 -0
- package/docs/testing.md +36 -7
- package/package.json +1 -1
- package/src/cwd-lease.ts +95 -6
- package/src/decision-session-store.ts +343 -37
- package/src/decision-worker.ts +41 -4
- package/src/index.ts +161 -82
- package/src/supervisor.ts +43 -13
- package/src/verifier.ts +5 -2
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.1](https://github.com/btnalit/pi-claude-supervisor/compare/v0.5.0...v0.5.1) (2026-09-14)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* harden single-worker recovery lifecycle ([14c01ec](https://github.com/btnalit/pi-claude-supervisor/commit/14c01ec610b1210eda1ea2d60269fad25b8b3575))
|
|
11
|
+
|
|
5
12
|
## [0.5.0](https://github.com/btnalit/pi-claude-supervisor/compare/v0.4.1...v0.5.0) (2026-09-14)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -17,6 +24,9 @@ All notable changes to this project will be documented here.
|
|
|
17
24
|
- Independent read-only Reviewer results with bounded findings and automatic repair rounds.
|
|
18
25
|
- JSONL malformed-record handling and duplicate result/permission suppression fixtures.
|
|
19
26
|
- Active JSONL request shutdown coverage and deterministic acceptance/review tests.
|
|
27
|
+
- Durable Decision Worker recovery claims with stale-owner reconciliation and explicit fail-closed takeover.
|
|
28
|
+
- Identity-bound tmux handoff cleanup and recovery/lease lifecycle coverage.
|
|
29
|
+
- Claude Code 2.1.270 bounded stability matrix evidence in `docs/stability-matrix-2.1.270.md`.
|
|
20
30
|
|
|
21
31
|
## [0.4.1](https://github.com/btnalit/pi-claude-supervisor/compare/v0.4.0...v0.4.1) (2026-09-14)
|
|
22
32
|
|
|
@@ -85,7 +95,7 @@ All notable changes to this project will be documented here.
|
|
|
85
95
|
- Historical Claude CLI 2.1.268 permission allow/deny and SIGTERM/SIGINT transport spike evidence; current release validation uses Claude CLI 2.1.270.
|
|
86
96
|
- Event-driven JSONL `control_request`/`result`/exit events, permission responses, persistent Pi Decision Worker automation, bounded duplicate/turn handling, and outbound human-intervention webhooks.
|
|
87
97
|
- Long-task defaults are now 100 automatic turns, 4 hours wall time and 20 minutes without output; Decision Worker API failures alert human operators directly instead of attempting an LLM fallback.
|
|
88
|
-
- Automatic Decision Worker sessions now persist as Pi JSONL with a 0600 task registry. Unclean Pi restarts expose explicit `/supervise recover <task-id>` recovery; Claude work is not silently duplicated.
|
|
98
|
+
- Automatic Decision Worker sessions now persist as Pi JSONL with a 0600 task registry. Unclean Pi restarts expose explicit `/supervise recover [--takeover] <task-id>` recovery; Claude work is not silently duplicated.
|
|
89
99
|
- Local tests and package-content checks.
|
|
90
100
|
|
|
91
101
|
### Limitations
|
package/README.cn.md
CHANGED
|
@@ -54,7 +54,7 @@ Claude CLI `2.1.270` 运行,跨版本兼容性不在本轮范围内。
|
|
|
54
54
|
/supervise start --spec ./task.json
|
|
55
55
|
/supervise poll
|
|
56
56
|
/supervise sessions
|
|
57
|
-
/supervise recover <task-id>
|
|
57
|
+
/supervise recover [--takeover] <task-id>
|
|
58
58
|
/supervise stop human requested stop
|
|
59
59
|
/supervise verify
|
|
60
60
|
/supervise approve <task-id> allow|deny [request-id]
|
|
@@ -94,17 +94,20 @@ Claude CLI `2.1.270` 运行,跨版本兼容性不在本轮范围内。
|
|
|
94
94
|
|
|
95
95
|
当前 webhook 是出站通知,不直接接受批准命令;批准或接管仍通过 Pi。
|
|
96
96
|
自动模式会将 Decision Worker 会话持久化到状态目录。Pi 非正常重启后,`/supervise sessions`
|
|
97
|
-
会显示 `recoverable` 任务;显式执行 `/supervise recover <task-id>` 会恢复 Decision Worker 上下文并
|
|
98
|
-
重新启动 Claude Worker
|
|
97
|
+
会显示 `recoverable` 任务;显式执行 `/supervise recover [--takeover] <task-id>` 会恢复 Decision Worker 上下文并
|
|
98
|
+
重新启动 Claude Worker,不会静默恢复或重复执行任务。旧 Pi 进程已退出且租约确认旧 Worker
|
|
99
|
+
进程组已消失且 cgroup 仍是真实、可读取的空边界时,才可显式添加 `--takeover`;缺失、仍存活或无法确认的 Worker 会被拒绝。持久 tmux
|
|
100
|
+
Worker 应使用 `adopt-tmux`,而不是 takeover。
|
|
99
101
|
自动模式下,Decision Worker 可以安全拒绝 `AskUserQuestion`,让 Claude 将问题转成普通文本,
|
|
100
102
|
再根据任务和仓库证据自动回答;无法确定时才升级人工。如需微信内闭环,需要另建带签名验证、
|
|
101
103
|
一次性 action token 和重放保护的入站 callback 服务。
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
`v0.5.0` 已完成并发布“多命令验收—独立只读 Reviewer—结构化修复轮次—再次验收”闭环。
|
|
104
106
|
任务可通过 API 或 JSON spec 提供 `goal`、`scope`、`constraints`、`forbidden` 和多个
|
|
105
107
|
`acceptance` 命令;旧的纯文本任务继续使用默认 `git diff --check`。Reviewer 只能使用
|
|
106
|
-
`read`、`grep`、`find`、`ls
|
|
107
|
-
`2.1.270
|
|
108
|
+
`read`、`grep`、`find`、`ls`,不会修改工作树或批准权限。短期剩余门禁是固定 Claude Code
|
|
109
|
+
`2.1.270` 的重复稳定性统计和 recovery 测试;协同多 Worker 属于后续独立开发阶段,
|
|
110
|
+
暂不把多版本兼容、sandbox、低权限和网络隔离作为本阶段门禁。
|
|
108
111
|
|
|
109
112
|
### tmux/PTY 交互模式
|
|
110
113
|
|
|
@@ -150,6 +153,11 @@ PTY 屏幕文字不是 Claude JSONL。权限/信任对话框和无法确定的 T
|
|
|
150
153
|
/supervise send <task-id> continue after checking the test failure
|
|
151
154
|
```
|
|
152
155
|
|
|
156
|
+
当前支持的是**独立任务会话并行**,不是共享工作树的协同多 Worker。后续多 Worker
|
|
157
|
+
开发任务会引入 parent/child 任务图、依赖、并发上限、结构化 handoff、汇总验收和
|
|
158
|
+
跨进程恢复,但不会放宽“一个 worktree 一个写入者”的边界,也不会自动 merge 或 publish。
|
|
159
|
+
该阶段应安排在固定 Claude `2.1.270` 稳定性统计和单 Worker recovery 语义完成之后。
|
|
160
|
+
|
|
153
161
|
Pull Request 必须通过聚合的 `CI / Quality gate`。Release Please 根据 Conventional Commits 创建版本 PR;维护者合并后,Release workflow 会针对精确 tag commit 重新验证,并通过受保护的 `npm` environment 使用 npm provenance 发布。
|
|
154
162
|
|
|
155
163
|
详细内容见 [engineering-plan.md](docs/engineering-plan.md)、[independent-review.md](docs/independent-review.md)、[architecture.md](docs/architecture.md)、[testing.md](docs/testing.md) 和 [releasing.md](docs/releasing.md)。
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ export PI_CLAUDE_SUPERVISOR_WORKER=claude
|
|
|
59
59
|
/supervise start inspect the current repository and report what should be changed
|
|
60
60
|
/supervise start --spec ./task.json
|
|
61
61
|
/supervise sessions
|
|
62
|
-
/supervise recover <task-id>
|
|
62
|
+
/supervise recover [--takeover] <task-id>
|
|
63
63
|
/supervise poll
|
|
64
64
|
/supervise poll all
|
|
65
65
|
/supervise send continue with read-only inspection
|
|
@@ -100,24 +100,36 @@ The supervisor allows only one active JSONL request per session: poll until its
|
|
|
100
100
|
yet exposed by the adapter. Multiple independent task sessions can run
|
|
101
101
|
concurrently when they use different canonical working directories or
|
|
102
102
|
worktrees; same-directory starts are rejected even when concurrent, and
|
|
103
|
-
`/supervise sessions` lists the sessions.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
`/supervise sessions` lists the sessions. This is independent-session
|
|
104
|
+
parallelism, not coordinated multi-worker collaboration. A future multi-worker
|
|
105
|
+
milestone will add explicit parent/child task graphs, dependencies, bounded
|
|
106
|
+
scheduling, structured handoffs, aggregate acceptance and graph-aware recovery;
|
|
107
|
+
it will not relax the one-writer-per-worktree rule or enable automatic
|
|
108
|
+
merge/publish. Unattended use still requires the remaining lifecycle, signal and
|
|
109
|
+
recovery checks. Host permissions and network access follow explicit caller
|
|
110
|
+
authorization and host policy; there is no automatic merge, deploy, release or
|
|
111
|
+
publish.
|
|
112
|
+
|
|
113
|
+
The `v0.5.0` automation milestone adds a structured acceptance pipeline:
|
|
109
114
|
multiple argv-based checks, an independent read-only Reviewer, bounded structured
|
|
110
115
|
findings and repair rounds. Legacy text tasks keep the default `git diff --check`.
|
|
111
116
|
The Reviewer only has `read`, `grep`, `find` and `ls`; it cannot edit files or grant
|
|
112
|
-
permissions.
|
|
117
|
+
permissions. The next gates are pinned Claude Code `2.1.270` stability statistics
|
|
118
|
+
and explicit session recovery. Coordinated multi-worker scheduling is a later
|
|
119
|
+
milestone; CI will use deterministic fake Workers/replay fixtures, and real
|
|
120
|
+
multi-worker Claude tests will remain authenticated manual Spikes. CLI
|
|
113
121
|
multi-version compatibility, sandboxing, low-privilege execution and network
|
|
114
122
|
isolation are not part of this milestone.
|
|
115
123
|
|
|
116
124
|
Automatic mode persists the Pi Decision Worker session under the configured state
|
|
117
125
|
directory. After an unclean Pi restart, `/supervise sessions` lists recoverable
|
|
118
|
-
tasks; `/supervise recover <task-id>` explicitly restores the Decision Worker
|
|
126
|
+
tasks; `/supervise recover [--takeover] <task-id>` explicitly restores the Decision Worker
|
|
119
127
|
context and starts a new Claude Worker. It never silently resumes or duplicates
|
|
120
|
-
work.
|
|
128
|
+
work. If the old Pi owner is dead, add `--takeover` only after the lease proves
|
|
129
|
+
the old Worker's process group is gone and its cgroup is a real, readable empty
|
|
130
|
+
boundary; missing or unverifiable Worker evidence is refused.
|
|
131
|
+
For a persistent tmux Worker, use explicit `adopt-tmux` instead of takeover.
|
|
132
|
+
The adapter intentionally does not inherit arbitrary host environment variables.
|
|
121
133
|
Pass credentials through an explicit `WorkerStartInput.env` in an embedding
|
|
122
134
|
integration. For the built-in command, opt in to named variables, for example
|
|
123
135
|
`PI_CLAUDE_SUPERVISOR_WORKER_ENV=ANTHROPIC_API_KEY`.
|
package/docs/architecture.md
CHANGED
|
@@ -27,6 +27,28 @@ directories/worktrees; same-cwd and parent/child cwd starts are rejected before
|
|
|
27
27
|
spawn, including concurrent starts, to prevent uncoordinated edits. Pending starts
|
|
28
28
|
are also awaited during Pi shutdown.
|
|
29
29
|
|
|
30
|
+
### Multi-worker boundary and roadmap
|
|
31
|
+
|
|
32
|
+
The current implementation supports multiple **independent** task sessions, not
|
|
33
|
+
coordinated shared-worktree editing. Every active session must own a
|
|
34
|
+
non-overlapping canonical cwd/worktree and has an isolated Supervisor,
|
|
35
|
+
watchdog, Worker handle and acceptance/review loop. The shared EventLog is only
|
|
36
|
+
an audit stream; it is not a collaboration or authorization channel.
|
|
37
|
+
|
|
38
|
+
A future multi-worker scheduler must introduce an explicit parent/child task
|
|
39
|
+
graph, roles, dependencies, bounded concurrency and structured handoff
|
|
40
|
+
artifacts. Child Workers must communicate through validated evidence and event
|
|
41
|
+
references rather than another Worker's control channel. Each child is accepted
|
|
42
|
+
independently; the parent can complete only after aggregate acceptance and
|
|
43
|
+
independent Review. Integration, conflict resolution, merge and publication
|
|
44
|
+
remain explicit human-controlled operations in a separate integration worktree.
|
|
45
|
+
|
|
46
|
+
Recovery and shutdown must be graph-aware: a parent with an unknown child state
|
|
47
|
+
cannot complete, cancellation must propagate within a bounded budget, and Pi
|
|
48
|
+
shutdown must leave every child either cleanup-verified or explicitly
|
|
49
|
+
recoverable. This work is scheduled after single-worker stability and session
|
|
50
|
+
recovery, not by relaxing the current cwd lease rule.
|
|
51
|
+
|
|
30
52
|
## MVP transport
|
|
31
53
|
|
|
32
54
|
`ProcessWorkerAdapter` uses `node:child_process.spawn` with:
|
|
@@ -126,9 +148,12 @@ registry before spawning Claude. The default registry is
|
|
|
126
148
|
may point all Pi processes at an alternate shared directory. Canonical paths
|
|
127
149
|
conflict with both their parents and descendants, and the registry lock
|
|
128
150
|
serializes acquisition across independent Pi processes. A lease is released
|
|
129
|
-
only after the adapter confirms the worker and its descendant cleanup
|
|
130
|
-
unconfirmed lease left by a crashed Pi is intentionally retained
|
|
131
|
-
operator
|
|
151
|
+
only after the adapter confirms the worker and its descendant cleanup. An
|
|
152
|
+
unconfirmed lease left by a crashed Pi is intentionally retained. Ordinary
|
|
153
|
+
recovery refuses it; an operator may use `recover --takeover` only when the old
|
|
154
|
+
owner is dead, the Worker process group is gone, and the lease independently
|
|
155
|
+
reads a real empty cgroup boundary for the old Worker. Missing or unverifiable
|
|
156
|
+
Worker evidence still requires manual cleanup rather than unsafe reclamation.
|
|
132
157
|
An explicitly adopted tmux session may hand off an existing lease only after
|
|
133
158
|
its owner identity is no longer live and its canonical cwd, tmux session/socket,
|
|
134
159
|
pane id, pane PID/start time, and pane command all match; ordinary starts
|
|
@@ -170,7 +195,8 @@ duplicate turns. The adapter also exposes event subscriptions for `result`,
|
|
|
170
195
|
`control_request`, permission requests and process exit. Automatic mode routes
|
|
171
196
|
those events to a persistent, read-only Pi Decision Worker; its Pi session JSONL
|
|
172
197
|
and task mapping are persisted under the supervisor state directory. After an
|
|
173
|
-
unclean Pi restart, recovery is explicit: `/supervise recover
|
|
198
|
+
unclean Pi restart, recovery is explicit: `/supervise recover [--takeover]
|
|
199
|
+
<task-id>` restores
|
|
174
200
|
the Decision Worker context and starts a new Claude Worker. It does not silently
|
|
175
201
|
resume or duplicate a task. It does not poll to detect turn completion. A watchdog timer remains only as a deadlock safety
|
|
176
202
|
fallback. Permission actions pass through `evaluatePermission` and can be
|
package/docs/engineering-plan.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pi Claude Supervisor 完整方案
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 文档状态:`v0.5.0` 已发布 / 后续开发路线图
|
|
4
4
|
> 目标项目目录:`pi-claude-supervisor`
|
|
5
5
|
> 适用对象:W、项目负责人、实现人员、评审人员
|
|
6
6
|
|
|
@@ -36,7 +36,7 @@ Claude Code Worker
|
|
|
36
36
|
4. Supervisor 因误判导致无限循环、危险操作或不可审计的修改。
|
|
37
37
|
5. 人工无法随时接管或恢复任务。
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
**当前状态:`v0.5.0` 已完成“验收—独立 Review—自动修复—再验收”闭环并正式发布。下一阶段先完成固定 Claude Code `2.1.270` 的稳定性统计和恢复语义,再推进有边界的多 Worker 协作;低权限用户、OS sandbox 与网络隔离仍是后续安全加固,不作为当前主线硬性阻塞。**
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -74,7 +74,9 @@ Claude Code 适合作为实际开发 Worker,但在长时间任务中可能出
|
|
|
74
74
|
- 不自动 merge、deploy 或 release。
|
|
75
75
|
|
|
76
76
|
当前扩展已支持多个独立任务会话并行推进,但不允许活动会话共享同一
|
|
77
|
-
工作目录。事件日志由跨进程锁协调,状态和 watchdog
|
|
77
|
+
工作目录。事件日志由跨进程锁协调,状态和 watchdog 按会话隔离。这里要区分两种
|
|
78
|
+
“多 Worker”:**独立会话并行**已经属于当前能力;**有依赖、交接和汇总验收的协同多
|
|
79
|
+
Worker**属于后续开发任务,不能通过简单地放宽 cwd 限制来实现。
|
|
78
80
|
|
|
79
81
|
暂不支持:
|
|
80
82
|
|
|
@@ -979,3 +981,52 @@ Reviewer 必须使用独立 Pi session,只允许 `read`、`grep`、`find`、`l
|
|
|
979
981
|
- OS sandbox、低权限执行和网络隔离;
|
|
980
982
|
- 自动 merge、deploy、release、publish;
|
|
981
983
|
- 多 Worker 在同一工作树协作。
|
|
984
|
+
|
|
985
|
+
## 21. 后续开发路线图
|
|
986
|
+
|
|
987
|
+
`v0.5.0` 的发布不代表所有自动化目标都已完成。后续任务按“稳定性 → 恢复 → 协同
|
|
988
|
+
调度 → 安全加固”推进;多 Worker 可以纳入开发任务,但应作为独立阶段,不能与当前
|
|
989
|
+
单 Worker 稳定性门禁混在一起。
|
|
990
|
+
|
|
991
|
+
### 21.1 短期:稳定性收尾
|
|
992
|
+
|
|
993
|
+
- 完成真实 Claude Code `2.1.270` 重复 Spike:普通任务连续 10 次,权限和问题回退各至少 5 次;
|
|
994
|
+
- 补齐 replay:多轮修复、验收失败修复、repair budget 耗尽、takeover、recover 和 Pi shutdown;
|
|
995
|
+
- 补齐边界测试:Reviewer 流式输出上限、`DecisionSessionStore.list()` 任务 ID 校验、跨进程恢复和超时/输出截断;
|
|
996
|
+
- 继续观察 npm `0.5.0`、GitHub Release 资产、provenance 和回滚路径;
|
|
997
|
+
- 验收标准:无重复动作、错误 complete、未清理 Worker 或未审计的自动放行。
|
|
998
|
+
|
|
999
|
+
### 21.2 中期:恢复能力
|
|
1000
|
+
|
|
1001
|
+
- 设计安全的 Claude session resume;明确 `--resume` 与实时 PTY attach 的边界;
|
|
1002
|
+
- 完善 takeover、recover、Pi shutdown、Worker 崩溃和部分完成的状态语义;
|
|
1003
|
+
- 增加跨进程恢复端到端测试,包括 cwd lease、Decision Worker session、Worker 身份和事件日志一致性;
|
|
1004
|
+
- 恢复失败必须进入 `HUMAN_REQUIRED`,不能静默重放原始任务或重复发送输入。
|
|
1005
|
+
|
|
1006
|
+
### 21.3 后续:多 Worker 协作与调度
|
|
1007
|
+
|
|
1008
|
+
第一阶段只做**独立 worktree 的多 Worker 编排**,不允许共享工作树写入。建议拆成以下
|
|
1009
|
+
开发任务:
|
|
1010
|
+
|
|
1011
|
+
1. **任务图与角色模型**:增加 `parentTaskId`、Worker role、`dependsOn`、worktree、handoff
|
|
1012
|
+
artifact 和子任务状态;明确 root task 与 child task 的审计关联。
|
|
1013
|
+
2. **受限调度器**:实现并发上限、依赖就绪、全局时间/修复预算、取消传播和失败隔离;
|
|
1014
|
+
不让任意 Worker 自行启动、停止或批准另一个 Worker。
|
|
1015
|
+
3. **结构化交接**:Worker 之间只通过受限 artifact、事件引用和验收报告交接,不直接共享
|
|
1016
|
+
控制通道;交接内容必须经过 schema 校验和大小限制。
|
|
1017
|
+
4. **汇总验收**:每个 child 先独立验收,root task 再汇总目标、diff、测试和 Reviewer 结果;
|
|
1018
|
+
冲突、缺失证据或任一 P0/P1 自动升级人工。
|
|
1019
|
+
5. **恢复与关闭**:支持单个 child、整棵任务图和 Pi shutdown 的一致性恢复;父任务不能在
|
|
1020
|
+
子任务状态未知时报告 `completed`。
|
|
1021
|
+
6. **冲突检测和人工整合**:只允许在独立 integration worktree 中进行显式整合;不自动
|
|
1022
|
+
merge/publish,冲突和整合动作必须保留人工控制权。
|
|
1023
|
+
|
|
1024
|
+
多 Worker 阶段的最小验收矩阵:两个独立 Worker 并行、依赖顺序、一个 Worker 失败、取消
|
|
1025
|
+
传播、重复交接、工作树冲突、单 child 恢复、整棵任务图恢复和 shutdown 中断。通过这些
|
|
1026
|
+
门禁后,才评估是否需要更复杂的 lead-worker 或动态任务分解。
|
|
1027
|
+
|
|
1028
|
+
### 21.4 后置:安全加固
|
|
1029
|
+
|
|
1030
|
+
- CLI 多版本兼容矩阵;
|
|
1031
|
+
- OS sandbox、低权限执行、网络隔离/allowlist;
|
|
1032
|
+
- 更深的供应链、SBOM、密钥隔离和生产监控。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Recovery follow-up review
|
|
2
|
+
|
|
3
|
+
The independent read-only review of the recovery changes initially returned
|
|
4
|
+
`BLOCK` with three P1 findings, one P2 finding, and a coverage note. The findings
|
|
5
|
+
were addressed in this follow-up.
|
|
6
|
+
|
|
7
|
+
- Stale `starting`/`registered`/`recovered_idle` claims now persist recovery
|
|
8
|
+
owner PID/start time, are reconciled only after the old owner and Worker
|
|
9
|
+
boundary are independently gone, and return to an explicit `interrupted`
|
|
10
|
+
state.
|
|
11
|
+
- `--takeover` requires a dead owner, dead Worker/process group, and a real,
|
|
12
|
+
readable empty cgroup boundary; missing or unverifiable Worker evidence is
|
|
13
|
+
rejected.
|
|
14
|
+
- Worker registration and `recovered_idle` transitions require an active record,
|
|
15
|
+
persist atomically, and are read back and checked before the recovered session
|
|
16
|
+
is exposed.
|
|
17
|
+
- Adopted tmux handoff reports the replaced task and closes its old Decision
|
|
18
|
+
session mapping only after worker identity registration succeeds.
|
|
19
|
+
- Session closure now carries cleanup evidence and intent; uncertain cleanup
|
|
20
|
+
retains the active record and cwd lease.
|
|
21
|
+
|
|
22
|
+
Validation: `npm run check`, `npm run build`, the pinned Claude Code 2.1.270
|
|
23
|
+
question matrix (5/5 verified), and the bounded stability evidence in
|
|
24
|
+
[`stability-matrix-2.1.270.md`](stability-matrix-2.1.270.md).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Claude Code 2.1.270 stability matrix
|
|
2
|
+
|
|
3
|
+
Run date: 2026-09-14. The executable was resolved as
|
|
4
|
+
`/home/yancao/.local/share/mise/installs/claude/2.1.270/claude` and reported
|
|
5
|
+
`2.1.270 (Claude Code)`. Runs used the authenticated local provider and
|
|
6
|
+
`scripts/spike-claude-automation.mjs`; no Claude `--resume` was used.
|
|
7
|
+
|
|
8
|
+
## Required matrix (120 s bounded run)
|
|
9
|
+
|
|
10
|
+
| Scenario | Runs | Completed + verified | Fail-closed outcomes |
|
|
11
|
+
| --- | ---: | ---: | --- |
|
|
12
|
+
| ordinary task | 10 | 9 | 1 stopped at the bounded deadline without verification |
|
|
13
|
+
| permission handling | 5 | 5 | 0 |
|
|
14
|
+
| question handling | 5 | 5 | 0 |
|
|
15
|
+
|
|
16
|
+
The ordinary outlier was not treated as success: the spike exited non-zero,
|
|
17
|
+
kept verification false, and stopped rather than retrying or replaying the task.
|
|
18
|
+
This is the intended timeout fail-closed behavior. An earlier question spike also
|
|
19
|
+
escalated after invalid Decision Worker output; it was likewise not counted as a
|
|
20
|
+
success. The five-run question matrix above was then rerun with a 180 s bound and
|
|
21
|
+
all five completed with `verified=true` and zero human interventions.
|
|
22
|
+
|
|
23
|
+
## Bounded follow-up
|
|
24
|
+
|
|
25
|
+
Three additional ordinary and three additional question runs with the same
|
|
26
|
+
pinned executable and a 180 s bound also completed with `verified=true` and zero
|
|
27
|
+
human interventions. These runs support a provider-latency explanation for the
|
|
28
|
+
120 s outliers; they do not turn an outlier into a success.
|
|
29
|
+
|
|
30
|
+
The deterministic replay, acceptance, recovery-state, lease, cleanup and
|
|
31
|
+
package checks remain the CI evidence. Authenticated Claude runs are manual
|
|
32
|
+
release evidence only.
|
package/docs/testing.md
CHANGED
|
@@ -67,8 +67,10 @@ guarantee.
|
|
|
67
67
|
The extension persists each automatic Decision Worker session as Pi JSONL plus a
|
|
68
68
|
0600 task mapping. Recovery is explicit and safe: after an unclean Pi restart,
|
|
69
69
|
`/supervise sessions` shows the task as `recoverable`, and `/supervise recover
|
|
70
|
-
<task-id>` restores the Decision Worker history before starting a new
|
|
71
|
-
Worker.
|
|
70
|
+
[--takeover] <task-id>` restores the Decision Worker history before starting a new
|
|
71
|
+
Claude Worker. `--takeover` is accepted only when the old Pi owner is dead, the
|
|
72
|
+
Worker process group is gone, and its cgroup is a real readable empty boundary;
|
|
73
|
+
persistent tmux sessions use `adopt-tmux`.
|
|
72
74
|
Run the permission and signal probes explicitly when validating a CLI release:
|
|
73
75
|
|
|
74
76
|
```bash
|
|
@@ -94,7 +96,9 @@ response, and an exact result marker; it also records metadata only.
|
|
|
94
96
|
|
|
95
97
|
For each release, pin and record the validated Claude Code version, resolved
|
|
96
98
|
executable path, and model. The spikes reject an unpinned/mismatched executable
|
|
97
|
-
version. For this release the validated version is `2.1.270` with model `opus
|
|
99
|
+
version. For this release the validated version is `2.1.270` with model `opus`;
|
|
100
|
+
the bounded matrix and its fail-closed outliers are recorded in
|
|
101
|
+
[`docs/stability-matrix-2.1.270.md`](stability-matrix-2.1.270.md).
|
|
98
102
|
Record:
|
|
99
103
|
|
|
100
104
|
1. exact version and resolved executable path;
|
|
@@ -152,7 +156,8 @@ the managed process group.
|
|
|
152
156
|
|
|
153
157
|
## Near-term automation acceptance gate
|
|
154
158
|
|
|
155
|
-
The
|
|
159
|
+
The `v0.5.0` implementation of the acceptance—independent Review—repair—reacceptance
|
|
160
|
+
loop is shipped. The remaining gate focuses on real stability for the pinned
|
|
156
161
|
Claude Code `2.1.270` CLI. It does not add a multi-version matrix or wait for
|
|
157
162
|
OS sandbox, low-privilege or network-isolation work.
|
|
158
163
|
|
|
@@ -186,6 +191,30 @@ The adapter/replay matrix must cover:
|
|
|
186
191
|
|
|
187
192
|
Real Claude tests remain authenticated manual Spikes and are pinned to
|
|
188
193
|
`2.1.270`; they are not part of normal CI. Normal CI runs deterministic fake
|
|
189
|
-
Worker and replay fixtures.
|
|
190
|
-
ordinary automatic runs and at least five runs each
|
|
191
|
-
handling, with no duplicate action, false completion
|
|
194
|
+
Worker and replay fixtures. The short-term stability gate is still pending and
|
|
195
|
+
must include ten consecutive ordinary automatic runs and at least five runs each
|
|
196
|
+
for permission and question handling, with no duplicate action, false completion
|
|
197
|
+
or unreaped Worker.
|
|
198
|
+
|
|
199
|
+
## Future multi-worker test plan
|
|
200
|
+
|
|
201
|
+
Multiple independent task sessions already run concurrently when their canonical
|
|
202
|
+
cwd/worktrees do not overlap. This is not yet coordinated multi-worker
|
|
203
|
+
collaboration. The future multi-worker milestone must be tested as a task graph,
|
|
204
|
+
not as unrestricted shared-worker access.
|
|
205
|
+
|
|
206
|
+
Required deterministic and integration coverage:
|
|
207
|
+
|
|
208
|
+
- two independent Workers with separate worktrees and aggregated parent status;
|
|
209
|
+
- dependency ordering and a blocked child that must not start early;
|
|
210
|
+
- child failure, timeout, cancellation propagation and bounded global budgets;
|
|
211
|
+
- schema-validated handoff artifacts with duplicate/oversized/stale handoffs;
|
|
212
|
+
- conflicting diffs detected before integration, with no same-worktree writes;
|
|
213
|
+
- independent child acceptance followed by root-task aggregate acceptance and Review;
|
|
214
|
+
- single-child recovery, whole-graph recovery and Pi shutdown during scheduling;
|
|
215
|
+
- no child can grant permissions, send control input to another child or bypass Policy Gate;
|
|
216
|
+
- explicit human-controlled integration in a separate worktree; no automatic merge or publish.
|
|
217
|
+
|
|
218
|
+
The multi-worker gate should be added only after the pinned single-worker stability
|
|
219
|
+
and recovery gates pass. CI should use fake Workers and replay fixtures; authenticated
|
|
220
|
+
Claude multi-worker Spikes remain manual and version-pinned.
|
package/package.json
CHANGED
package/src/cwd-lease.ts
CHANGED
|
@@ -36,8 +36,25 @@ export interface CwdLeaseHandoff {
|
|
|
36
36
|
tmuxSocket?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Explicit recovery takeover. This is intentionally separate from ordinary
|
|
41
|
+
* acquisition: an old Pi owner may have died while its Worker survived.
|
|
42
|
+
*/
|
|
43
|
+
export interface CwdLeaseTakeover {
|
|
44
|
+
taskId: string;
|
|
45
|
+
/** Run before the old lease is removed; failure leaves the old lease intact. */
|
|
46
|
+
beforeReplace?: (lease: CwdLeaseRecord) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CwdLeaseAcquireOptions {
|
|
50
|
+
handoff?: CwdLeaseHandoff;
|
|
51
|
+
takeover?: CwdLeaseTakeover;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
export interface CwdLeaseHandle {
|
|
40
55
|
readonly record: CwdLeaseRecord;
|
|
56
|
+
/** Task id whose lease was explicitly handed off/taken over, if any. */
|
|
57
|
+
readonly replacedTaskId?: string;
|
|
41
58
|
updateWorker(worker: CwdLeaseWorker): Promise<void>;
|
|
42
59
|
release(): Promise<void>;
|
|
43
60
|
}
|
|
@@ -63,7 +80,9 @@ export class CwdLeaseStore {
|
|
|
63
80
|
return this.#directory;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
async acquire(cwd: string, taskId: string, transport: CwdLeaseTransport, options:
|
|
83
|
+
async acquire(cwd: string, taskId: string, transport: CwdLeaseTransport, options: CwdLeaseAcquireOptions = {}): Promise<CwdLeaseHandle> {
|
|
84
|
+
assertTaskId(taskId);
|
|
85
|
+
if (options.takeover) assertTaskId(options.takeover.taskId);
|
|
67
86
|
const canonicalCwd = await realpath(resolve(cwd));
|
|
68
87
|
const now = new Date().toISOString();
|
|
69
88
|
const lease: CwdLeaseRecord = {
|
|
@@ -77,6 +96,7 @@ export class CwdLeaseStore {
|
|
|
77
96
|
updatedAt: now,
|
|
78
97
|
worker: { transport },
|
|
79
98
|
};
|
|
99
|
+
let replacedTaskId: string | undefined;
|
|
80
100
|
await this.#withLock(async () => {
|
|
81
101
|
const leases = await this.#readAll();
|
|
82
102
|
let handoffLease: CwdLeaseRecord | undefined;
|
|
@@ -88,23 +108,35 @@ export class CwdLeaseStore {
|
|
|
88
108
|
continue;
|
|
89
109
|
}
|
|
90
110
|
if (!pathsOverlap(existing.cwd, canonicalCwd)) continue;
|
|
111
|
+
if (options.takeover?.taskId === existing.taskId
|
|
112
|
+
&& existing.cwd === canonicalCwd
|
|
113
|
+
&& await canTakeoverLease(existing)) {
|
|
114
|
+
await options.takeover.beforeReplace?.(existing);
|
|
115
|
+
replacedTaskId = existing.taskId;
|
|
116
|
+
await rm(this.#path(existing.leaseId), { force: true });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
91
119
|
throw new Error(`working-directory lease is held by task ${existing.taskId}: ${redactText(existing.cwd)}`);
|
|
92
120
|
}
|
|
93
121
|
await this.#write(lease);
|
|
94
|
-
if (handoffLease)
|
|
122
|
+
if (handoffLease) {
|
|
123
|
+
replacedTaskId = handoffLease.taskId;
|
|
124
|
+
await rm(this.#path(handoffLease.leaseId), { force: true });
|
|
125
|
+
}
|
|
95
126
|
});
|
|
96
|
-
return this.#handle(lease);
|
|
127
|
+
return this.#handle(lease, replacedTaskId);
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
async list(): Promise<CwdLeaseRecord[]> {
|
|
100
131
|
return this.#withLock(() => this.#readAll());
|
|
101
132
|
}
|
|
102
133
|
|
|
103
|
-
#handle(initial: CwdLeaseRecord): CwdLeaseHandle {
|
|
134
|
+
#handle(initial: CwdLeaseRecord, replacedTaskId?: string): CwdLeaseHandle {
|
|
104
135
|
let current = { ...initial, worker: initial.worker ? { ...initial.worker } : undefined };
|
|
105
136
|
let released = false;
|
|
106
137
|
return {
|
|
107
138
|
get record() { return current; },
|
|
139
|
+
get replacedTaskId() { return replacedTaskId; },
|
|
108
140
|
updateWorker: async (worker) => {
|
|
109
141
|
if (released) throw new Error("cwd lease has already been released");
|
|
110
142
|
assertWorker(worker);
|
|
@@ -157,8 +189,8 @@ export class CwdLeaseStore {
|
|
|
157
189
|
async #write(lease: CwdLeaseRecord): Promise<void> {
|
|
158
190
|
await this.#ensureDirectory();
|
|
159
191
|
const target = this.#path(lease.leaseId);
|
|
160
|
-
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
161
|
-
await writeFile(temporary, `${JSON.stringify(lease, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
192
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
193
|
+
await writeFile(temporary, `${JSON.stringify(lease, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
162
194
|
await rename(temporary, target);
|
|
163
195
|
await chmod(target, 0o600);
|
|
164
196
|
}
|
|
@@ -252,6 +284,10 @@ async function processIdentityLive(pid: number, expectedStartTime?: string): Pro
|
|
|
252
284
|
const currentStartTime = await processStartTime(pid);
|
|
253
285
|
if (currentStartTime && expectedStartTime) return currentStartTime === expectedStartTime;
|
|
254
286
|
if (currentStartTime) return true;
|
|
287
|
+
return processExists(pid);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function processExists(pid: number): Promise<boolean> {
|
|
255
291
|
try {
|
|
256
292
|
process.kill(pid, 0);
|
|
257
293
|
return true;
|
|
@@ -260,6 +296,55 @@ async function processIdentityLive(pid: number, expectedStartTime?: string): Pro
|
|
|
260
296
|
}
|
|
261
297
|
}
|
|
262
298
|
|
|
299
|
+
async function processGroupExists(pid: number): Promise<boolean> {
|
|
300
|
+
try {
|
|
301
|
+
process.kill(-pid, 0);
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return error instanceof Error && /EPERM/u.test(error.message);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function cgroupHasProcesses(path: string): Promise<boolean> {
|
|
309
|
+
// Lease files are untrusted state. Never read an arbitrary path during
|
|
310
|
+
// takeover; only a real, canonical cgroup below the kernel cgroup root is
|
|
311
|
+
// eligible for this check. Missing/unreadable evidence is not proof of an
|
|
312
|
+
// empty cgroup: a descendant may have escaped before the cgroup disappeared.
|
|
313
|
+
const cgroupRoot = resolve("/sys/fs/cgroup");
|
|
314
|
+
const cgroupPath = resolve(path);
|
|
315
|
+
if (cgroupPath === cgroupRoot || !cgroupPath.startsWith(`${cgroupRoot}/`)) return true;
|
|
316
|
+
try {
|
|
317
|
+
const cgroupInfo = await lstat(cgroupPath);
|
|
318
|
+
if (!cgroupInfo.isDirectory() || cgroupInfo.isSymbolicLink()) return true;
|
|
319
|
+
const canonicalPath = await realpath(cgroupPath);
|
|
320
|
+
if (canonicalPath !== cgroupPath || !canonicalPath.startsWith(`${cgroupRoot}/`)) return true;
|
|
321
|
+
const procsPath = join(canonicalPath, "cgroup.procs");
|
|
322
|
+
const procsInfo = await lstat(procsPath);
|
|
323
|
+
if (!procsInfo.isFile() || procsInfo.isSymbolicLink()) return true;
|
|
324
|
+
const contents = await readFile(procsPath, "utf8");
|
|
325
|
+
return contents.split(/\s+/u).some((pid) => /^\d+$/u.test(pid));
|
|
326
|
+
} catch {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function canTakeoverLease(lease: CwdLeaseRecord): Promise<boolean> {
|
|
332
|
+
// The old supervisor owner must be gone. A dead owner is not enough when
|
|
333
|
+
// the detached Worker itself is still alive.
|
|
334
|
+
if (await processExists(lease.ownerPid)) return false;
|
|
335
|
+
const worker = lease.worker;
|
|
336
|
+
if (!worker) return false;
|
|
337
|
+
if (worker.transport === "tmux") return false;
|
|
338
|
+
if (!worker.pid) return false;
|
|
339
|
+
if (await processExists(worker.pid)) return false;
|
|
340
|
+
if (await processGroupExists(worker.pid)) return false;
|
|
341
|
+
// A process-group check cannot see a setsid descendant. Explicit takeover
|
|
342
|
+
// therefore requires the verified cgroup boundary used by the adapter.
|
|
343
|
+
if (!worker.cgroupPath || !resolve(worker.cgroupPath).startsWith(`${resolve("/sys/fs/cgroup")}/`)) return false;
|
|
344
|
+
if (await cgroupHasProcesses(worker.cgroupPath)) return false;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
263
348
|
async function processStartTime(pid: number): Promise<string | undefined> {
|
|
264
349
|
try {
|
|
265
350
|
const statText = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
@@ -288,6 +373,10 @@ async function matchesHandoff(lease: CwdLeaseRecord, handoff: CwdLeaseHandoff):
|
|
|
288
373
|
return !(await processIdentityLive(lease.ownerPid, lease.ownerStartTime));
|
|
289
374
|
}
|
|
290
375
|
|
|
376
|
+
function assertTaskId(taskId: string): void {
|
|
377
|
+
if (!/^[0-9a-f-]{36}$/iu.test(taskId)) throw new Error("invalid cwd lease task id");
|
|
378
|
+
}
|
|
379
|
+
|
|
291
380
|
function assertWorker(worker: CwdLeaseWorker): void {
|
|
292
381
|
if (!["process-pipe", "jsonl", "pty", "tmux"].includes(worker.transport)
|
|
293
382
|
|| (worker.pid !== undefined && (!Number.isSafeInteger(worker.pid) || worker.pid < 1))
|