pi-crew 0.9.62 → 0.9.64
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 +92 -0
- package/README.md +1 -0
- package/agents/critic.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +162 -67
- package/package.json +5 -2
- package/src/agents/agent-config.ts +4 -0
- package/src/agents/agent-serializer.ts +1 -0
- package/src/agents/discover-agents.ts +8 -0
- package/src/config/role-tools.ts +49 -1
- package/src/prompt/prompt-runtime.ts +6 -0
- package/src/prompt/scratchpad-lifecycle.ts +605 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +42 -1
- package/src/runtime/child-pi/child-pi.ts +5 -0
- package/src/runtime/model/pi-args.ts +1 -1
- package/src/runtime/recovery/crash-recovery.ts +1 -1
- package/src/runtime/scratchpad/README.md +184 -0
- package/src/runtime/scratchpad/engine.ts +610 -0
- package/src/runtime/scratchpad/guest.ts +360 -0
- package/src/runtime/scratchpad/index.ts +22 -0
- package/src/runtime/scratchpad/protocol.ts +88 -0
- package/src/runtime/scratchpad/snapshot-lookup.ts +74 -0
- package/src/runtime/scratchpad/transform.ts +363 -0
- package/src/runtime/task-runner/child-executor.ts +48 -31
|
@@ -31,7 +31,7 @@ const createdTempDirs = new Set<string>();
|
|
|
31
31
|
* /tmp directory. Uses `userPiRoot()` so the path stays consistent with
|
|
32
32
|
* the rest of pi-crew (respects PI_TEAMS_HOME / PI_CODING_AGENT_DIR).
|
|
33
33
|
*/
|
|
34
|
-
function getPiTempBase(): string {
|
|
34
|
+
export function getPiTempBase(): string {
|
|
35
35
|
return path.join(userPiRoot(), "tmp");
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -98,7 +98,7 @@ export function readManifestWithTransientRetry(manifestPath: string, maxRetries
|
|
|
98
98
|
throw new Error(`unreachable: readManifestWithTransientRetry exhausted for ${manifestPath}`);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function shouldRecoverTask(task: TeamTaskState, deadMs: number): boolean {
|
|
101
|
+
export function shouldRecoverTask(task: TeamTaskState, deadMs: number): boolean {
|
|
102
102
|
if (task.status !== "running") return false;
|
|
103
103
|
if (!task.heartbeat) return true;
|
|
104
104
|
return task.heartbeat.alive === false || isWorkerHeartbeatStale(task.heartbeat, deadMs);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Spike go/no-go — pi-rlm → Node port (pattern 01+04+05+08+09)
|
|
2
|
+
|
|
3
|
+
> **Kết quả: ✅ GO** — 17/17 test GREEN, spawn subprocess Node thật (không mock, không Bun).
|
|
4
|
+
> Ngày: 2026-08-08. Spec: `../../rlm-apply-pi-crew.md` mục 5 + 7.1.
|
|
5
|
+
|
|
6
|
+
## Mục đích
|
|
7
|
+
|
|
8
|
+
Chứng minh 8 pattern FLAGSHIP của pi-rlm port sang Node thuần chạy được TRƯỚC khi đầu tư full FLAGSHIP. 2 invariant quyết định:
|
|
9
|
+
|
|
10
|
+
- **(a) Bindings survive mid-cell failure** (pattern 05): cell 1 `throw` giữa chừng → cell sau vẫn thấy biến đã gán trước throw.
|
|
11
|
+
- **(b) Namespace revives across process boundary** (pattern 08+09): engine 1 snapshot → kill → engine 2 (process mới) restore → cell mới đọc được biến.
|
|
12
|
+
|
|
13
|
+
## Cấu trúc
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
src/runtime/scratchpad/
|
|
17
|
+
├── protocol.ts (88 dòng) — fd3 + nonce + envelope, port 1:1 từ pi-rlm
|
|
18
|
+
├── transform.ts (363 dòng) — esbuild transformSync (strip) + acorn parse, decl→assignment, trailing-expr→setResult
|
|
19
|
+
├── guest.ts (330 dòng) — namespace proxy + with(SCOPE) + v8.serialize + AsyncLocalStorage, KHÔNG Bun
|
|
20
|
+
├── engine.ts (564 dòng) — EngineManager host, spawn(process.execPath, [--experimental-strip-types, guest.ts])
|
|
21
|
+
└── index.ts (22 dòng) — barrel
|
|
22
|
+
|
|
23
|
+
test/runtime/scratchpad/
|
|
24
|
+
├── protocol.test.ts (72 dòng)
|
|
25
|
+
├── transform.test.ts (59 dòng)
|
|
26
|
+
└── engine.spike.test.ts (126 dòng) — 2 invariant + phụ trợ, spawn subprocess thật
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Cách chạy
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd /home/bom/source/my_pi/pi-crew
|
|
33
|
+
node scripts/test-runner.mjs --test-force-exit 'test/runtime/scratchpad/**/*.test.ts'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Kết quả
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
# tests 17
|
|
40
|
+
# pass 17
|
|
41
|
+
# fail 0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Port Node quan trọng (không Bun)
|
|
45
|
+
|
|
46
|
+
| pi-rlm (Bun) | port Node | xác nhận |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `bun:jsc serialize/deserialize` | `node:v8` serialize/deserialize | ✅ guest.ts:32, snapshot/restore pass invariant (b) |
|
|
49
|
+
| `Bun.Transpiler` (DCE off) | `esbuild` transformSync + acorn | ✅ transform.ts, trailing-expr không bị drop |
|
|
50
|
+
| `Bun.inspect` | `node:util` inspect | ✅ guest.ts:31 |
|
|
51
|
+
| `spawn("bun", ["run", guest])` | `spawn(process.execPath, ["--experimental-strip-types", guest])` | ✅ engine.ts:173 |
|
|
52
|
+
| `Bun.$` guard / host bridge | **bỏ** (không cần cho spike) | — |
|
|
53
|
+
|
|
54
|
+
## Kết luận
|
|
55
|
+
|
|
56
|
+
**GO** — FLAGSHIP được xanh-light. Bước tiếp: FLAGSHIP Phase 1 (tool `execute` opt-in per role + snapshot vào artifact-store), rồi Phase 2 (crash-resume trong retry loop).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# Phase 2 — Crash-Resume (cross-attempt restore)
|
|
61
|
+
|
|
62
|
+
Phase 2 closes the crash-resume loop: a worker attempt N+1 (retry / crash-recovery
|
|
63
|
+
re-queue / manual re-run) automatically revives the namespace from the previous
|
|
64
|
+
attempt's snapshot, **without the model knowing** (besides a one-line notice).
|
|
65
|
+
|
|
66
|
+
## Flow
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
attempt N (scratchpad worker)
|
|
70
|
+
└─ execute → EngineManager → guest namespace
|
|
71
|
+
└─ flush (debounce / shutdown-quit / post-ok) → writeArtifact REDACTED
|
|
72
|
+
→ artifactsRoot/scratchpad/<taskId>.attempt-<i>.snapshot.json
|
|
73
|
+
│ worker dies / fails / cancelled
|
|
74
|
+
▼
|
|
75
|
+
attempt N+1 spawn (prepareSpawnContext — the single choke point)
|
|
76
|
+
└─ findLatestScratchpadSnapshot(artifactsRoot, taskId) ← snapshot-lookup.ts
|
|
77
|
+
└─ latest MTIME wins (model-fallback `i` resets each retry round → number is
|
|
78
|
+
NOT write-order; tie-break: lowest attempt = newest round)
|
|
79
|
+
└─ env PI_CREW_SCRATCHPAD_RESTORE (+ RESTORE_MTIME hint)
|
|
80
|
+
▼
|
|
81
|
+
attempt N+1 worker (scratchpad-lifecycle.ts)
|
|
82
|
+
└─ FIRST execute call → re-validate at READ time (D10) → restoreState →
|
|
83
|
+
notice "[scratchpad] restored N vars from attempt-K; restored:[...]; failed:[...]"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Env keys (parent → worker, set in `prepareSpawnContext` scratchpad gate)
|
|
87
|
+
|
|
88
|
+
| Key | Direction | Purpose |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `PI_CREW_SCRATCHPAD` | parent→worker | "1" arms the execute tool (dormant gate) |
|
|
91
|
+
| `PI_CREW_TASK_ID` | parent→worker | snapshot relativePath provenance |
|
|
92
|
+
| `PI_CREW_ATTEMPT` | parent→worker | model-fallback index (per-attempt suffix) |
|
|
93
|
+
| `PI_CREW_ARTIFACTS_ROOT` | parent→worker | writeArtifact root |
|
|
94
|
+
| `PI_CREW_SCRATCHPAD_SNAPSHOT` | parent→worker | WRITE target (raw temp, never in artifacts) |
|
|
95
|
+
| `PI_CREW_SCRATCHPAD_RESTORE` | parent→worker | **Phase 2**: READ source (redacted artifact) |
|
|
96
|
+
| `PI_CREW_SCRATCHPAD_RESTORE_MTIME` | parent→worker | **Phase 2**: swap-detection HINT (forgeable, not authn) |
|
|
97
|
+
| `PI_CREW_KIND` / `PI_CREW_PARENT_PID` / `PI_CREW_GUEST` | engine→guest | **Phase 2 (D5)**: guest reports the WORKER pid (not the leader's) so an orphaned guest is flagged by the zombie scanner |
|
|
98
|
+
|
|
99
|
+
## Guards (D1–D13)
|
|
100
|
+
|
|
101
|
+
- **D1/D1b'** lookup at spawn, latest mtime, tie-break lowest attempt.
|
|
102
|
+
- **D3** restore once per session, lazy on first execute (D7 invariant kept).
|
|
103
|
+
- **D4** redacted secret → literal `"***"` placeholder (guest special-case; base64
|
|
104
|
+
of a real value is never `"***"`).
|
|
105
|
+
- **D5/MAJOR-S1** production wiring: `getScratchpadEngine` overrides
|
|
106
|
+
`PI_CREW_PARENT_PID=worker pid` (pure inheritance leaves guests LIVE forever).
|
|
107
|
+
- **D6** cap 4 MiB two-sided: write-side raw byteLength (trim failed→50 only when
|
|
108
|
+
over cap); read-side file size + guest per-var 256 KiB.
|
|
109
|
+
- **D10** restore path re-validated at READ time (TOCTOU): containment +
|
|
110
|
+
filename pattern + lstat regular + size + mtime pin.
|
|
111
|
+
- **D11** restore fail-open: any failure logs + continues on an empty namespace.
|
|
112
|
+
- **D12** scan strict (lstat/pattern/regular) — cross-agent poisoning is NOT a new
|
|
113
|
+
trust boundary (same-uid team worker already writes artifacts); notice lists
|
|
114
|
+
var names so the model re-verifies.
|
|
115
|
+
- **D13** base64 round-trip check before deserialize (flat redaction can inject
|
|
116
|
+
`"***"` into a valid payload → silent corruption → failed[]).
|
|
117
|
+
|
|
118
|
+
## Threat model (Phase 2 additions — defense-in-depth within the same-uid boundary)
|
|
119
|
+
|
|
120
|
+
- **v8.deserialize of restore content is unauthenticated** (no HMAC). Bounded by
|
|
121
|
+
the 4 MiB file cap + 256 KiB per-var cap + same-uid artifact dir. A planted
|
|
122
|
+
snapshot with a crafted v8 blob can run deserialize gadgets in the guest — but
|
|
123
|
+
the guest already runs at full worker trust (it holds provider keys + broker
|
|
124
|
+
token), so this does not cross the existing boundary. An HMAC over the payload
|
|
125
|
+
is a Phase 2.5/3 hardening if artifacts ever land in a shared location.
|
|
126
|
+
- **Secret-at-rest under a benign key name** persists as base64 for the run's
|
|
127
|
+
retention (structural redaction is key-name based, best-effort). A sanitized-
|
|
128
|
+
namespace policy is a Phase 2.5 concern.
|
|
129
|
+
- **Restore-source trust = same as any artifact**: a same-uid team worker can
|
|
130
|
+
plant a matching-named snapshot; restore removes the "model chooses to read"
|
|
131
|
+
step, so the notice deliberately lists the revived var names.
|
|
132
|
+
|
|
133
|
+
## CI note
|
|
134
|
+
|
|
135
|
+
The `test/runtime/scratchpad/*` spike tests (incl. the D1/D4/D6/D13 restore pins)
|
|
136
|
+
are NOT wired into `npm run test:unit` (which globs `test/unit/**`). Run them
|
|
137
|
+
directly per the Phase runbook: `node scripts/test-runner.mjs --test-force-exit
|
|
138
|
+
test/runtime/scratchpad/restore-e2e.spike.test.ts`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
# Phase 3 — Cancellation: kill-and-restore (verified) + atomic snapshot
|
|
143
|
+
|
|
144
|
+
## Kill-and-restore ALREADY WORKS (no new handler needed)
|
|
145
|
+
|
|
146
|
+
A worker killed via SIGTERM is flushed by the **existing** F3 quit-path — no
|
|
147
|
+
Phase 3 worker-side handler is required. The chain (verified against installed
|
|
148
|
+
pi 0.80.3/0.84.1):
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
parent abort() / timeout / drain
|
|
152
|
+
└─ child-pi killProcessTree → group SIGTERM
|
|
153
|
+
└─ pi print-mode signal handler (modes/print-mode.js:31-44 registerSignalHandlers)
|
|
154
|
+
└─ disposeRuntime() → runtimeHost.dispose()
|
|
155
|
+
└─ emitSessionShutdownEvent({ reason: "quit" }) [awaited]
|
|
156
|
+
└─ scratchpad-lifecycle F3 handler (reason === "quit" gate passes)
|
|
157
|
+
└─ performShutdownFlush: snapshot → writeArtifact (redact) → engine.kill
|
|
158
|
+
└─ process.exit(143)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The next attempt then restores from this F3 flush (Phase 2), closing the ≤1.5s
|
|
162
|
+
debounce gap. (pi-crew's own `extension/crew-cleanup.ts:98` also installs a
|
|
163
|
+
worker SIGTERM handler for child-process cleanup; both coexist.)
|
|
164
|
+
|
|
165
|
+
## Phase 3 hardening
|
|
166
|
+
|
|
167
|
+
- **Atomic `snapshotState` (D2')**: the raw snapshot temp is now written via
|
|
168
|
+
`temp + rename` (same dir → atomic). Eliminates the theoretical torn-write
|
|
169
|
+
race between the debounce timer and the F3 quit flush.
|
|
170
|
+
- **EngineBusyError: SKIPPED** (confirmed). The spike dropped it (engine.ts:15);
|
|
171
|
+
the engine serializes concurrent `execute` calls via a FIFO queue, and
|
|
172
|
+
ping-before-execute (`scratchpad-lifecycle.ts`) already detects a wedged guest.
|
|
173
|
+
Re-adding a busy-reject would break the queue contract for no new coverage.
|
|
174
|
+
- **Eviction is a non-scenario**: live agents are in-process SDK sessions
|
|
175
|
+
(`live-session-runtime.ts`), `abort()` is in-process (no SIGTERM, no worker
|
|
176
|
+
process). Scratchpad is never armed in the host process (gate requires
|
|
177
|
+
`PI_CREW_KIND=subagent`), so there is nothing to flush on eviction.
|
|
178
|
+
|
|
179
|
+
## Pin test
|
|
180
|
+
|
|
181
|
+
`test/runtime/scratchpad/sigterm-kill-restore.spike.test.ts` (gated
|
|
182
|
+
`PI_CREW_TEST_REAL_MODEL=1`) spawns a real `pi --mode json -p` worker, runs one
|
|
183
|
+
`execute` cell, sends SIGTERM inside the debounce window, and asserts an F3
|
|
184
|
+
artifact appears with a SIGTERM-time mtime. Skipped by default (CI-safe).
|