pi-crew 0.11.52 → 0.11.54
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 +149 -7
- package/dist/index.mjs +508 -508
- package/package.json +1 -1
- package/src/runtime/broker/protocol/msg-inbox.ts +27 -3
- package/src/runtime/child-pi/child-pi-timers.ts +25 -0
- package/src/runtime/foreground-control.ts +164 -116
- package/src/runtime/pi-spawn.ts +112 -2
- package/src/runtime/task-runner.ts +17 -0
- package/src/runtime/verification/verification-gates.ts +103 -7
- package/src/runtime/verification/verify-gate.ts +209 -21
- package/src/state/event-log/event-log.ts +187 -63
- package/src/worktree/worktree-manager.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,154 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.54] — deep-review verified fix set, waves A–G + closing hardenings (2026-10-31)
|
|
4
|
+
|
|
5
|
+
**Fixed (product).** The v0.11.53 deep review's 15 findings were cold-verified by three independent verifiers
|
|
6
|
+
(13 CONFIRMED / 2 PARTIAL / 0 refuted); this batch implements the seven fix waves against `8e70ab6f`. Four
|
|
7
|
+
findings are deliberately deferred with documented backstops (closing note). Wave-by-wave:
|
|
8
|
+
|
|
9
|
+
- **wA foreground-control lock family (F1/F2)** — the producer "lock" was `mkdirSync(lockDir, { recursive: true })`:
|
|
10
|
+
recursive mkdir NEVER throws EEXIST, so it provided zero mutual exclusion and its contention branches were dead
|
|
11
|
+
code. Replaced with module-level `withForegroundControlLock` (foreground-control.ts) replicating the proven
|
|
12
|
+
event-log dir-lock protocol: the non-recursive mkdir IS the mutex; 5s timeout; 10s mtime-stale reclaim; dead-pid
|
|
13
|
+
keep-waiting fast path (never steal on a pid-lookup race); `"wx"` pid file; PID-guarded release. F2: the
|
|
14
|
+
interrupt-guard consumer ack did an unlocked read→mutate→whole-file write that clobbered producer appends — it
|
|
15
|
+
now acquires the SAME module-level lock, re-reads inside it, and acks by request `id` (tail fallback for legacy
|
|
16
|
+
id-less control files). Retry contends ONLY on EEXIST; any other IO error fails fast
|
|
17
|
+
(`foreground-control.lock-acquire-failed`) instead of spinning to the timeout. Behavior delta (per spec, event-log
|
|
18
|
+
pattern semantics): a dead owner no longer releases the lock immediately — only an aged mtime (>10s) reclaims.
|
|
19
|
+
Dedup with `withEventLogAppendLockSync` into a shared helper is a noted follow-up (parallel-wave file ownership).
|
|
20
|
+
- **wB event-log buffered-flush timer family (F3/F4/F5)** — F3: a flush's `finally` deleted the timer entry without
|
|
21
|
+
re-arming, so events appended during an in-flight flush sat un-timered until the next append; it now re-arms when
|
|
22
|
+
leftovers exist (identity guard so a fresh handle armed by another append is never orphaned; delay = the queue's
|
|
23
|
+
last-used `bufferMs` — `bufferedTimers` now stores `{ handle, bufferMs }`). F4: the in-flight tracker stored the
|
|
24
|
+
`.finally()` WRAPPER but compare-and-deleted the ORIGINAL promise — entries were never deleted (leak; the drain
|
|
25
|
+
helpers' self-delete no-op path was dead code); it now compares the stored wrapper. F5: the armed handle is
|
|
26
|
+
cleared at grab time (terminal-event/drain-triggered flushes no longer leave a stale timer firing a redundant
|
|
27
|
+
no-op flush); `flushBufferedQueuesSync` clears pending handles too. **Out-of-spec, flagged and kept**:
|
|
28
|
+
`flushEventLogBuffer()` now iterates the UNION of queued + in-flight paths — queue-only iteration skipped exactly
|
|
29
|
+
the paths its own await-inflight logic exists for, and the deterministic-read pin flaked ~1 run in 8 at baseline
|
|
30
|
+
HEAD (0/20 post-fix).
|
|
31
|
+
- **wC child-pi duplicate drain (F6)** — the final-drain 5s CEILING callback left the quiet-window poller armed:
|
|
32
|
+
ceiling fires first + child goes quiet-alive ⇒ the poller fired a SECOND forced drain (duplicate `final_drain` +
|
|
33
|
+
SIGTERM) and overwrote `hardKillTimer`, orphaning the ceiling's timer ⇒ duplicate `hard_kill`. The ceiling now
|
|
34
|
+
clears the poller (mirroring its own self-cancel) and hard-kill emission is idempotent at all three arm sites
|
|
35
|
+
(`hardKilled` gate). `settled || childExited` guards untouched; both transports arm this path (child-pi.ts /
|
|
36
|
+
child-pi-rpc.ts). Adjacent, left for triage: a NEW final assistant event after a quiet-drain can still re-arm one
|
|
37
|
+
extra `final_drain`, but hard-kill is bounded to one.
|
|
38
|
+
- **wD verify-gate shared-cwd untracked harness (S-1, HIGH)** — the dispatch-pinned bindings (treeOid/cleanTree/memo
|
|
39
|
+
key) bind TRACKED content only; in shared-cwd mode the gate ran its checks in the same cwd a WRITE-role executor
|
|
40
|
+
had just written to, so a tampered gitignored harness (node_modules shims) graded its own gate and skipped the LLM
|
|
41
|
+
verifier. New `applySharedCwdHarnessGuard` (verify-gate.ts), chained after the W2 guard at BOTH the memo-hit and
|
|
42
|
+
fresh-verdict PASS sites: any WRITE-role task that EXECUTED in the gate cwd (fail-closed status set incl.
|
|
43
|
+
`cancelled`; worktree-carrying records excluded; realpath-aware cwd compare) ⇒ refuse the deterministic skip and
|
|
44
|
+
force the spawn with reason `"shared-cwd-untracked-harness"`. Worktree-mode skip behavior unchanged; new guard
|
|
45
|
+
metric `crew.verification.verifier_gate_shared_cwd_guard_total`. Wiring deviation (deliberate, leader notified):
|
|
46
|
+
task-runner.ts passes host-owned task records (verifier's own excluded) into the pre-gate (+17 lines) — reading
|
|
47
|
+
tasks.json from the gate itself would be M1-7 self-grading ground truth. Documented residual: a worker forging
|
|
48
|
+
its own tasks.json record (role/status) still evades — state-store integrity lane. `worktree.linkNodeModules:
|
|
49
|
+
true` carries the matching trust-assumption comment (worktree-manager.ts) plus a docs/trust-model.md note.
|
|
50
|
+
- **wE msg.inbox run-level lane gating (S-2)** — legacy bare-runId ("runId-fallback") worker connections could read
|
|
51
|
+
the orchestrator's run-level inbox lane (wait.*/delegate.request already reject non-compound auth). `msg.inbox`
|
|
52
|
+
now rejects `taskId === undefined && authMatchKind !== "compound"` with `forbidden` + a `PI_CREW_BROKER_TASK_ID`
|
|
53
|
+
migrate hint (same error shape as wait-auth). The orchestrator key is compound BY CONSTRUCTION, so the legitimate
|
|
54
|
+
run-level reader is unaffected; undefined kind (pre-hello) fails closed; task-lane reads unchanged.
|
|
55
|
+
- **wF gate allowlist hardening (S-3)** — `printenv` dropped from `ALLOWED_GATE_COMMAND_PATTERNS` (it turned any
|
|
56
|
+
allowlisted gate into an environment-dump read primitive whose output lands in world-readable artifacts).
|
|
57
|
+
`grep`/`head`/`rg`/`tail` stay allowlisted only as cwd-scoped evidence extractors: new `validateGateReaderPathArgs`
|
|
58
|
+
rejects absolute and `~`-prefixed path args (one-quote-char strip closes the unbalanced-quote whitespace-split
|
|
59
|
+
bypass); runner patterns untouched. Deliberate residual: `..`-traversal and attached option values (`rg -f/etc/x`)
|
|
60
|
+
still pass — cheap follow-up. Known collateral (out-of-wave-ownership, escalated to leader): `verify-gate.test.ts`
|
|
61
|
+
"grep evidence lines" (absolute tmpdir arg; one-line relative fix) and `verification-env-sanitize.test.ts` (4
|
|
62
|
+
`printenv` probes; recommended tmpdir Makefile `@printenv` recipe) stay red until reworked.
|
|
63
|
+
- **wG pi PATH fallback validation (S-4)** — a `resolvePiCliScript()` miss returned bare `{ command: "pi" }`, spawned
|
|
64
|
+
via inherited PATH with NO validation, while explicit `PI_TEAMS_PI_BIN` IS validated. New `resolveCommandOnPath`
|
|
65
|
+
(which(1)-equivalent: PATH order, X_OK, PATHEXT) + `validatePathResolvedPi` (the same `isWithinAllowedPrefixes` +
|
|
66
|
+
realpath predicate as explicit-bin validation): a validated POSIX hit spawns by ABSOLUTE path (no child-side
|
|
67
|
+
re-resolution under a mutated env); win32 keeps bare `"pi"` (spawn cannot exec PATHEXT without a shell —
|
|
68
|
+
documented); an out-of-prefix hit THROWS loud diagnostics mirroring the invalid-PI_TEAMS_PI_BIN UX, while NO hit
|
|
69
|
+
on PATH keeps the legacy bare-`"pi"` behavior. Allowed prefixes extended minimally with `~/.pi/agent/bin` +
|
|
70
|
+
`~/.pi/agent/install` — the Pi-managed installer layout, live-verified legitimate and previously uncovered
|
|
71
|
+
(deliberately NOT all of `~/.pi/agent`, so `extensions/`/`cache/` never become auto-executable prefixes).
|
|
72
|
+
- **Closing hardenings (leader-approved rulings, same working tree, 2026-10-31):**
|
|
73
|
+
- **S-1 underscore-role bypass (HIGH follow-up):** the guard's `permissionForRole(record.role)` used the RAW role
|
|
74
|
+
while the tool-granting layer normalizes underscore→hyphen (agent-config.ts `normalizeRoleKey`), so
|
|
75
|
+
`test_engineer`/`cold_verifier`/`chain_executor` stayed write-capable at the worker while the guard
|
|
76
|
+
default-denied them to read_only — guard inert exactly for the roles it exists to catch. Fixed in verify-gate.ts
|
|
77
|
+
(mirrored `normalizeRoleKeyForGuard`; centralization TODO left at the three other un-normalized copies —
|
|
78
|
+
pre-execution.ts, spawn-policy.ts, prompt-builder.ts — out of this run's scope).
|
|
79
|
+
- **S-1 ancestor-cwd containment (M):** `taskRecordSharesGateCwd` exact-compared cwds; a WRITE-role task running
|
|
80
|
+
at an ANCESTOR cwd (task cwd=/ws, gate cwd=/ws/pi-crew) writes into the gate cwd all the same — a record now
|
|
81
|
+
counts as sharing when its realpath'd cwd equals OR CONTAINS the realpath'd gate cwd (fail-closed realpath
|
|
82
|
+
fallback preserved).
|
|
83
|
+
- **Guard-refused PASS never memoizes (M):** the memo record ran BEFORE the S-1 guard chained, so an uncertifiable
|
|
84
|
+
PASS could replay as a skip in a later run whose taskRecords are absent/empty. `recordVerifyGateMemo` is now
|
|
85
|
+
gated on the refusal reason (only `shared-cwd-untracked-harness`; the W2 refusal keeps recording — its memo-hit
|
|
86
|
+
path re-checks probes every replay). Fresh+memoized guard application unchanged.
|
|
87
|
+
- **S-4 realpath-degrade closure (LOW):** a prefix-valid PATH hit whose `realpathSync` probe failed for a
|
|
88
|
+
non-prefix reason (transient EACCES/TOCTOU/symlink loop) returned `undefined` → caller degraded to the
|
|
89
|
+
UNVALIDATED bare-`"pi"` command (the exact S-4 primitive). It now throws skip-with-diagnostic — a
|
|
90
|
+
found-and-prefix-valid candidate never degrades to the unvalidated fallback.
|
|
91
|
+
- **S-3 reader-path containment (LOW; closes wF's deliberate `..`/attached-value residual):**
|
|
92
|
+
`validateGateReaderPathArgs` now also rejects `..`-relative escapes (each non-flag arg resolved against the
|
|
93
|
+
execution cwd — threaded `executeCommand → validateGateCommand → validateGateReaderPathArgs` — with
|
|
94
|
+
containment enforcement), Windows absolute forms (`C:/…`, `\\host\share`), attached option values carrying a
|
|
95
|
+
path (`-f/etc/x`), and denies `rg --pre`/`--pre=*` outright (arbitrary preprocessor execution). Cwd-relative
|
|
96
|
+
evidence extraction (`grep -m1 . dist.txt`, in-cwd `sub/../x` round-trips) still passes.
|
|
97
|
+
- **Collateral test repairs + docs:** the 4 wave-F `printenv` env-sanitize probes reworked to the allowlisted
|
|
98
|
+
`make test` entry with a tmpdir Makefile `@printenv VAR` recipe (npm-run pattern; printenv NOT re-added to the
|
|
99
|
+
allowlist; win32-skipped like the prior fixture); the verify-gate grep-evidence test now uses the cwd-relative
|
|
100
|
+
`dist.txt`; trust-model.md documents the win32 spawn fallback + `~/.pi/agent/{bin,install}` same-uid residuals
|
|
101
|
+
and the memo/cross-run closure context.
|
|
102
|
+
|
|
103
|
+
**Deferred (documented residuals, untouched):** F7 heartbeat PID-recycle (stale-reconciler 30-min trust cap is the
|
|
104
|
+
designed backstop), F8 64KiB handshake cap (needs a chunked handshake protocol — feature, not a fix), S-5 unsigned
|
|
105
|
+
read-only RPC (backward-compat; live-control already fails closed), S-7 webhook DNS-residual SSRF (RFC1918
|
|
106
|
+
deliberately allowed — internal webhooks are a use case).
|
|
107
|
+
|
|
108
|
+
Tests: 29 new regression tests across the waves — wA 3 (subprocess-harness race pins: 3 producers × 16 writes, all
|
|
109
|
+
48 survive; guard-cycler vs hammering producer, 0 lost; legacy id-less ack fallback), wB 3 (deterministic F3/F4/F5
|
|
110
|
+
pins via the exported lock seam), wC 3 (mock-timer scenarios incl. the exact F6 repro: ceiling-first + quiet ⇒
|
|
111
|
+
exactly one `final_drain` + one `hard_kill`), wD 8 (6 engine incl. memo-hit interplay + 2 e2e), wE 2 (replacing the
|
|
112
|
+
test that pinned the vulnerable behavior), wF 5 (+ reworked P-4 cap fixture to a cwd-relative temp file), wG 5
|
|
113
|
+
(PATH resolution/validation). Every wave: `test:critical` 127/127, typecheck, biome green; wA and wC
|
|
114
|
+
mutation-verified (stash-revert reproduces the bug live). Closing hardenings add 5 more (S-1 underscore-role,
|
|
115
|
+
ancestor-cwd, memo-suppression pins D7–D9; reader-path containment; realpath-degrade) and rework the 4 wave-F
|
|
116
|
+
collateral env-sanitize probes to `make test`.
|
|
117
|
+
|
|
118
|
+
## [0.11.53] — event-log in-flight flush await fix (macos CI catch) + Bug-030 RCA closed (2026-10-31)
|
|
119
|
+
|
|
120
|
+
**Fixed (product, caught by macos CI).** `flushOneEventLogBuffer` removed the queue from
|
|
121
|
+
`bufferedQueues` BEFORE its multi-turn async lock+append completed, so `flushEventLogBuffer()` /
|
|
122
|
+
`drainEventLogWriters()` were **no-ops for a path with an in-flight flush** — `await flushEventLogBuffer()` could
|
|
123
|
+
return while the batch's `appendFile` was still in flight (deterministic-await broken; the interrupt-guard
|
|
124
|
+
detached regression test read the log too early on the slower macos runner fs — CI run 41147573288; fast
|
|
125
|
+
disks hid the race locally). Same window on the exit path (`flushBufferedQueuesSync` cannot recover an
|
|
126
|
+
in-flight batch — documented limitation, sync-only). Fix: per-path in-flight flush tracker
|
|
127
|
+
(`inflightBufferFlushes`) chained single-entry per path; `flushEventLogBuffer()` and `drainEventLogWriters()`
|
|
128
|
+
await the in-flight flush FIRST, then flush whatever landed in the queue since. Pin:
|
|
129
|
+
`flushEventLogBuffer awaits an IN-FLIGHT flush` (deterministic read immediately after the await — no settling
|
|
130
|
+
sleep).
|
|
131
|
+
|
|
132
|
+
**Fixed (test-only).** `waitForPreGateEvents` (classifier-pre-gate) now THROWS on timeout with counts + path
|
|
133
|
+
instead of returning silently — the silent return turned a legit wait-timeout into a far-away
|
|
134
|
+
`TypeError: Cannot read properties of undefined (reading 'taskId')` on `preGateEvents(...)[0]!` (the [pg-10]
|
|
135
|
+
CI flake: 2 Node-22 shards, run 40797688830, 2026-10-29; rerun green). A timeout now fails AT the wait site
|
|
136
|
+
with diagnostics (found vs expected `task.verifier_pre_gate` events, eventsPath, budget).
|
|
137
|
+
|
|
138
|
+
**Bug-030 RCA (closed 2026-10-30): CI-runner I/O starvation, not an append-path bug.** Traced the full
|
|
139
|
+
delivery chain: `appendEventAsync` → per-path `asyncQueues` (in-process promise chain) →
|
|
140
|
+
`withEventLogLockAsync` (mkdir O_EXCL, 50ms retry, **5s lock timeout**) → `.seqlock` reserve → `appendFile`,
|
|
141
|
+
gated by the fire-and-forget `void` + best-effort `.catch` in `classifyVerifierPreGate`. The [pg-10] test has
|
|
142
|
+
no cross-process contention (per-test tmpdir, one process per test file), so lock acquisition is instant —
|
|
143
|
+
the missed wait was the append landing AFTER the old 3s wall-clock budget but well inside the 5s lock
|
|
144
|
+
timeout (the drop-path needs the lock timeout to throw AND be swallowed — a runner freeze, and the event
|
|
145
|
+
is best-effort diagnostic by design). The [pg-10] family has not recurred since (8 subsequent CI runs, incl.
|
|
146
|
+
v0.11.51/0.11.52 matrices). Hardened deterministically: `waitForPreGateEvents` now DRAINS the in-process
|
|
147
|
+
append queue each poll (`drainEventLogWriters(eventsPath)` — awaits the ACTUAL append instead of racing
|
|
148
|
+
wall-clock) and the budget is 10s — strictly above the 5s lock timeout, so a throw semantically means
|
|
149
|
+
"append dropped or failed", never merely "slower than 3s". New pin `[pg-11]`: the helper rejects with
|
|
150
|
+
diagnostics when nothing ever lands (never silent, never false-pass).
|
|
151
|
+
|
|
3
152
|
## [0.11.52] — live-catch backlog: cross-session cancel reaches inline runs + heartbeat-watcher false-positive class (2026-10-30)
|
|
4
153
|
|
|
5
154
|
**Fixed (runtime, live-caught run `team_20261030044630`).** Two gaps filed as backlog after the
|
|
@@ -135,13 +284,6 @@ halves) and the residuals deferred by w1b/w7a/w7b:
|
|
|
135
284
|
42 new regression tests. Owner-observed deployment checklists for the leads' impact confirmation remain in the
|
|
136
285
|
audit's NEEDS-VALIDATION.md.
|
|
137
286
|
|
|
138
|
-
## [Unreleased] — test hardening
|
|
139
|
-
|
|
140
|
-
**Fixed (test-only).** `waitForPreGateEvents` (classifier-pre-gate) now THROWS on timeout with counts + path
|
|
141
|
-
instead of returning silently — the silent return turned a legit wait-timeout into a far-away
|
|
142
|
-
`TypeError: Cannot read properties of undefined (reading 'taskId')` on `preGateEvents(...)[0]!` (the [pg-10]
|
|
143
|
-
CI flake: 2 Node-22 shards, run 40797688830, 2026-10-29; rerun green). A timeout now fails AT the wait site
|
|
144
|
-
with diagnostics (found vs expected `task.verifier_pre_gate` events, eventsPath, budget). Bug-030 RCA backlog item.
|
|
145
287
|
|
|
146
288
|
## [0.11.50] — LOW-1: PI_CREW_BROKER_STDIN fd3 handshake DEFAULT-ON ('0' opts out) (2026-10-28)
|
|
147
289
|
|