pi-crew 0.9.32 → 0.9.34

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
@@ -1,6 +1,168 @@
1
1
  # Changelog
2
2
 
3
- ## [0.9.32] — typescript 7 upgrade (2026-07-10)
3
+ ## [0.9.34] — performance + security audit quick wins (2026-07-11)
4
+
5
+ Shipped the **forward performance audit** follow-up: 5 batches of perf + security fixes derived from the forward audit (`team_20260711095156_8a8a56a3c7dff269`) and the bundled code + security review. 15 audit findings + 1 security fix all closed. End-to-end verified on a live `team` run (`team_20260711134929_498a39322bc756d9`, 3/3 tasks, 148k tokens, real `tasks.json`/`events.jsonl`/`heartbeat.json` produced). Typecheck clean, lint clean, biome format clean, 146/146 targeted tests pass, full unit suite cut at 2,894 with **0 failures** (test runner timed out only on long backoff tests).
6
+
7
+ Detailed methodology: [`docs/perf/performance-audit-report-2026-07.md`](docs/perf/performance-audit-report-2026-07.md) · commit history below · end-to-end validation: [`/tmp/perf-audit-validation.md`](/tmp/perf-audit-validation.md).
8
+
9
+ ### Performance
10
+
11
+ **Phase 2 — HIGH severity (P1, P2, P3)** + LOW (P10) + SEC-M1 (committed `f8e5629`):
12
+ - **P1** — `withQueue()` returns the same task reference when the computed queue matches `task.graph.queue`. Allocation-free on the common case; reference-stable for downstream selectors.
13
+ - **P2** — `saveRunManifest` / `saveRunManifestAsync` capture `manifestCache` before invalidating so the cached tasks array is reused on write-through repopulation. Eliminates per-call `JSON.parse(readFileSync(tasks.json))`.
14
+ - **P3** — `persistSingleTaskUpdate` reuses caller-supplied `fallbackTasks` on the first CAS attempt; only re-reads disk on actual contention.
15
+ - **P10** — `mergeTaskUpdatesPreservingTerminal` replaced its `O(N*M)` `.find()` + `.map()` nested loops with a single-pass `Map<id, task>` index (1000 → 120 ops).
16
+ - **SEC-M1** — `validateEndpoint` blocks `fe80::/10` (link-local), `fec0::/10` (site-local), `ff00::/8` (multicast), `::` (unspecified), and `::ffff:x.x.x.x` (IPv4-mapped) in addition to `::1` and `fd`/`fc`.
17
+
18
+ **Phase 3 — MEDIUM (P5, P7)** (committed `1e8d5b2`):
19
+ - **P5** — `isTargetNotSymlink()` cached for 1s on a per-file basis; `invalidateSymlinkSafeCache(dir)` flushes both cache layers.
20
+ - **P7** — 5 fire-and-forget call sites in `live-session-runtime.ts` migrated from sync `appendEvent` (with `sleepSync` lock) to `appendEventFireAndForget`. SIGTERM/AbortSignal handlers now fire promptly during high-frequency prompt-done churn.
21
+
22
+ **Phase 4 — MEDIUM-LOW (P6, P9)** (committed `824f428`):
23
+ - **P6** — `writeProgress()` replaced its O(N²) `findIndex` dedup with a single-pass `Map<path, ArtifactDescriptor>`. Added a content-skip cache (WeakMap on manifest) for back-to-back `applyPolicy` + `executeTeamRun` calls within the same millisecond.
24
+ - **P9** — `computeStablePrefixComponents()` gained a 60s TTL cross-run cache keyed on `(cwd, step.task)`. Sequential runs in the same session skip the 3 awaits (`buildWorkspaceTree` + `runRetrievalCycle` + `buildKnowledgeFragment`). `clearStablePrefixCache()` clears both cache layers.
25
+
26
+ **Phase 5 — remaining LOW tier (P11, P12, P13)** + P14 / P15 closure (committed `121cc55`):
27
+ - **P11 + P14** — `buildTaskGraphIndex()` identity-memoized via WeakMap. Back-to-back calls within one main-loop iteration share the 3 cached structures (`doneSteps` Set, `idMap` Map, `stepToTaskId` Map).
28
+ - **P12** — `mapConcurrent()` short-circuits on `length === 0` and `length === 1`.
29
+ - **P13** — Team-runner heartbeat interval: 30s → 60s. Stale threshold is 5min (5 ticks of slack).
30
+
31
+ **Biome auto-format** (committed `c329cc7`): organized imports + line-width reformat for the import introduced in P6 (`hashArtifactContent as hashContent` + `writeArtifact` alphabetized into existing group).
32
+
33
+ ### Verification
34
+
35
+ - **Typecheck**: `tsc --noEmit` clean (including strip-types `import('./index.ts')`).
36
+ - **Lint**: `biome check --linter-enabled=true` clean (1125 files, 0 errors).
37
+ - **Format**: `biome format --check .` clean.
38
+ - **Conflict markers / lazy imports**: clean.
39
+ - **Targeted unit tests**: **146/146 pass** across 20 test files directly touched by these changes:
40
+ - `atomic-write-symlink`, `atomic-write-edge-cases`, `atomic-write`, `artifact-store`
41
+ - `event-log`, `event-log-rotation`, `event-log-async-queue`
42
+ - `live-session-import-latch`, `live-session-context`, `live-session-health`, `live-session-health-cov`
43
+ - `parallel-utils`, `team-runner-merge`, `team-runner-heartbeat`, `task-runner-heartbeat`
44
+ - `heartbeat-watcher`, `heartbeat-aggregator`, `heartbeat-gradient`
45
+ - `prompt-builder-cov`, `task-graph-scheduler`
46
+ - `otlp-ssrf`
47
+ - **Full unit suite**: 2,894 pass with **0 failures** at the time-of-test cutoff (test runner timed out only on retry/backoff tests with 1–15s waits).
48
+ - **End-to-end**: a real `team action='run' workflow='research' goal='extract P1/P2/P3/SEC-M1 ...' ` (`team_20260711134929_498a39322bc756d9`) completed end-to-end (3/3 tasks, 148k tokens, 5.5 minutes) with valid artifacts:
49
+ - `manifest.json` 23kB (P2 cached write-through stamp exercised)
50
+ - `tasks.json` 73kB (P3 first-attempt reuse exercised)
51
+ - `events.jsonl` 50kB, 132 events (P7 async fire-and-forget exercised)
52
+ - `heartbeat.json` valid (P13 60s interval exercised)
53
+ - `progress.md` (P6 dedup exercised)
54
+ - **Validation report**: `/tmp/perf-audit-validation.md` (9.2kB) verifies each fix site at file:line against current HEAD `121cc55`.
55
+
56
+ ### Tests Added (12 new)
57
+
58
+ - `atomic-write-symlink.test.ts`: 2 P5 cache invariants
59
+ - `parallel-utils.test.ts`: 2 P12 single-item + empty-array fast paths
60
+ - `task-graph-scheduler.test.ts`: 2 P14 cache-by-reference + invalidation
61
+ - `prompt-builder-cov.test.ts`: 2 P9 cross-run cache + invalidation
62
+ - `otlp-ssrf.test.ts`: 5 SEC-M1 IPv6 prefix tests (fe80::/10, fec0::/10, ff00::/8, ::, ::ffff:)
63
+
64
+ ### Honest framing
65
+
66
+ - **False-positive catch in production**: the live-validation report (`/tmp/perf-audit-validation.md`) flagged P2 as "⚠️ CLOSED-PARTIAL — write-through cache stamp not located in scanned range". Manual verification at `state-store.ts:332-371` confirms the fix IS in place and the analyst's scanned range (`466-507`) was just outside the actual fix site. Documented in the validation report.
67
+ - **Exceeds spec**: SEC-M1 implementation blocks more IPv6 reserved prefixes than the audit requested (audit cited `fe80:` + `fec0:`; implementation also adds `ff00:`, `::`, `::ffff:`). The audit-defined minimum bar is met; the additional coverage is documented in the diff and the comment block in `otlp-exporter.ts`.
68
+ - **Backstop analysis**: P2's "invalidate-before-write" is intentional (preserved across the fix). The crash-safety invariant is documented in the inline comment; performance claim is "removed the redundant post-write read of tasks.json", not "removed invalidation".
69
+
70
+ ## [0.9.33] — correctness hardening + reverse audit (2026-07-11)
71
+
72
+ Shipped the **forward performance audit** quick wins, then ran a **reverse audit** (6 parallel bug-hunters → firsthand verification) that surfaced 9 real correctness bugs the forward audit missed. **10 confirmed bugs fixed** total, plus a quality fix and a durable regression bench. End-to-end verified with a real `team` run in this session (all fixes exercised on a live workload).
73
+
74
+ Detailed methodology and per-finding verdicts: [`docs/perf/performance-audit-report-2026-07.md`](docs/perf/performance-audit-report-2026-07.md) · benchmark: [`test/bench/terminal-persist-blocking.bench.ts`](test/bench/terminal-persist-blocking.bench.ts)
75
+
76
+ ### Correctness — reverse audit (HIGH/CRITICAL, all verified firsthand)
77
+
78
+ The reverse audit ran 6 parallel bug-hunters across concurrency, state machine, resume, cancel, prune, and event-log/artifact paths. Each agent finding was treated as a lead and independently verified by reading the actual code before any fix — out of ~25 reports, **9 were confirmed real bugs**, several were re-evaluated as WONTFIX (backstopped, defensive-as-is, or fix-was-incorrect), and the rest remain unverified leads. Every shipped fix has a regression test proven to fail without the fix and pass with it.
79
+
80
+ | # | Bug | Severity | Commit | File |
81
+ |---|-----|----------|--------|------|
82
+ | 1 | **FIND-1/STATE-1** — immediate atomic writes did **not** cancel pending coalesced writes. A `persistSingleTaskUpdate` coalesced write (50 ms debounce) would fire **after** the merge's immediate `saveRunTasksAsync` and overwrite the fresh merge result with a stale single-task snapshot — losing other workers' results, attempt enrichment, and `refreshTaskGraphQueues`. Also closed the crash window where `tasks.json` showed `running` while `events.jsonl` already showed `completed` → recovery re-ran the completed task (duplicate tokens + duplicate side effects). | **CRITICAL** | `3b8446a` | `src/state/atomic-write.ts` |
83
+ | 2 | **R2** — `handleResume` loaded the manifest+tasks **before** the run lock and used the stale snapshot inside the lock → `resetTasks` mapped a just-completed task back to `queued` → `executeTeamRun` re-executed it (duplicate tokens + duplicate side effects). Sibling handlers (`cancelOrphanedRuns`, `reconcileAllStaleRuns`) already re-read inside the lock; resume did not. | HIGH | `e733dba` | `src/extension/team-tool.ts` |
84
+ | 3 | **R1** — `handleResume` had **no `ownerSessionId` check** (unlike `handleRetry`/`handleCancel`). Another session could resume (and re-execute) a run it doesn't own, racing the owning session. | HIGH | `e733dba` | `src/extension/team-tool.ts` |
85
+ | 4 | **P1** — `handleForget` deleted run state without killing the background runner or terminating live agents (unlike `handleCancel`). Forgetting a running run orphaned the runner + child Pi workers (separate process groups via `setsid`) — they kept executing and consuming API tokens until natural completion. The `orphan-worker-registry` only tracks the runner PID, not individual child PIDs. | HIGH | `ca43c54` | `src/extension/team-tool/lifecycle-actions.ts` |
86
+ | 5 | **F1** — the batch loop did `if (results.length === 0) break;`. `results` is empty **only** when every ready task was hook-skipped (`batchTasks → dispatchUnits → []`). The skipped tasks are now terminal, so their downstream dependents become ready on the next iteration — but `break` exited the loop entirely, leaving downstream tasks `queued` while the post-loop code fell through to a false-green `updateRunStatus(manifest, "completed")`. Trigger: a `before_task_start` hook that blocks all ready tasks when downstream tasks depend on them. | HIGH | `118f9ac` | `src/runtime/team-runner.ts` |
87
+ | 6 | **CANCEL-1** — the batch merge used the in-memory `tasks` closure variable as the base for `mergeTaskUpdatesPreservingTerminal`. An external cancel (`handleCancel`) writes `cancelled` to `disk.tasks`, but the in-memory tasks don't reflect it → the merge overwrote the just-written cancellation with the stale in-memory view, permanently losing per-task cancel state. Reachability: background run + SIGTERM race (cancel writes + sends SIGTERM, but the runner's merge, already queued, acquires the lock after cancel released it and overwrites before the kill lands). | MED-HIGH | `ae40c96` | `src/runtime/team-runner.ts` |
88
+ | 7 | **CANCEL-2** — the post-merge cancel handler correctly re-cancelled the **manifest** status to `cancelled`, but saved tasks **without** re-applying the cancel to non-terminal tasks. Result: `status=cancelled` but tasks showed `completed/running` — inconsistent, and broke `handleRetry`'s filter for failed/cancelled tasks. Sibling of the batch-loop cancel check at `team-runner.ts:~925` which DID re-cancel tasks; the post-merge handler just forgot. Complements CANCEL-1 by handling signal-abort-during-merge (where CANCEL-1's disk-cancel never landed). | MEDIUM | `fd476b1` | `src/runtime/team-runner.ts` |
89
+ | 8 | **CANCEL-3 / F3** — `shouldMergeTaskUpdate` blocked `failed→completed` resurrection but missed the symmetric dangerous terminal→terminal flips: `cancelled→completed` (resurrection) and `completed→failed` (success demotion). Both are invalid per the task transition table (`contracts.ts`: terminal statuses only transition to `queued` on retry). A worker that completed after the task was cancelled could flip the settled `cancelled` status back to `completed`; a stale failed result arriving after completion could demote `completed` to `failed`. | MEDIUM | `3f237df` | `src/runtime/team-runner.ts` |
90
+ | 9 | **P2** — `pruneFinishedRuns` deleted `stateRoot` (`<crewRoot>/state/runs/<runId>/`) and `artifactsRoot`, but worktrees live at `<crewRoot>/state/worktrees/<runId>/` — a separate path that `fs.rmSync` does not touch. Every pruned worktree-using run leaked its worktree dir + git branch. `handleForget` already called `cleanupRunWorktrees`; prune did not. | MEDIUM | `733f0be` | `src/extension/run-maintenance.ts` |
91
+ | 10 | **P3** — `isFinished` included `"blocked"` as terminal, but blocked is NOT a terminal status (per `TEAM_RUN_STATUS_TRANSITIONS`, blocked can transition to `running/cancelled/failed` — e.g. plan-approval-pending, waiting-for-mailbox-response, scheduler stall). Pruning a blocked run destroys recoverable state: a plan the user needs to approve, or a mailbox wait the user needs to answer. Truly stuck blocked runs are handled by the stale-reconciler/crash-recovery, not by prune. | MEDIUM | `733f0be` | `src/extension/run-maintenance.ts` |
92
+ | 11 | **EL-1** — `appendEventAsync` reserves a seq, yields across `appendFile`/`fsync` awaits; a concurrent sync `appendEvent` can reserve+persist a higher seq (sidecar=6), then the async path resumes and persists its **lower** seq → sidecar regresses below the file's true max. `nextSequence` trusted the regressed sidecar unconditionally → on restart returned a duplicate seq. Fix: `nextSequence` takes `max(stored, scanSequence(eventsPath))` when trusting the sidecar, so a regressed sidecar cannot cause duplicate sequence numbers. Cost: `scanSequence` on cache-miss paths (already done in the else branch). | MEDIUM | `d27c65f` | `src/state/event-log.ts` |
93
+ | 12 | **EL-2** — `process.on("exit")` / `SIGTERM` / `SIGINT` / `uncaughtException` handlers called `flushEventLogBuffer()` / `drainAsyncQueues()` (async). The `exit` handler is **sync-only** and Node does **not** await the returned promises, so buffered events (`task.progress` via `appendEventBuffered` etc.) were silently lost on every process termination. Additionally, `flushOneEventLogBuffer` **dequeues** (`bufferedQueues.delete`) **before** the await — even if the write were awaited, the dequeue happens first, so a failed/abandoned write leaves events dequeued-without-persisted. The async `.catch(() => {})` calls also created floating promises that prevented `beforeExit` from firing. Fix: new `flushBufferedQueuesSync()` using the sync event-log lock + `appendFileSync` + fsync + persistSequence; all four handlers now call it + clear `asyncQueues`, recovering buffered events before the process dies. | MEDIUM | `d27c65f` | `src/state/event-log.ts` |
94
+
95
+ ### Correctness — forward audit (NEW-C3)
96
+
97
+ - **NEW-C3** — terminal manifest+tasks persistence race. `runTeamTask` wrote `manifest.json` (immediate) and `tasks.json` (via `persistSingleTaskUpdate` → `saveRunTasksCoalesced`, 50 ms debounce) **outside any lock**, racing the team-runner batch merge which writes both under `withRunLock`. A worker could persist its terminal artifacts → the merge's `loadRunManifestById` reads the worker-written manifest → merge overwrites with the worker's stale `tasks` (which lacks the merge's attempt enrichment + graph recompute) → silent data loss. Fix: wrap `saveRunManifest` + `persistSingleTaskUpdate` in a single `withRunLockSync` so the whole terminal state lands atomically. (`a03d244`)
98
+
99
+ ### Performance — forward audit quick wins (Phase 5 — remaining LOW-tier P11/P12/P13/P14)
100
+
101
+ Closing out the LOW-tier items from the audit. The HIGH and MEDIUM-tier items (Phase 2-4) are already shipped.
102
+
103
+ - **P11 + P14 (LOW)** — `buildTaskGraphIndex` (and transitively all callers through `ensureIndex`) now memoizes by array reference via a WeakMap. Back-to-back calls with the same tasks array (common within a single main-loop iteration) reuse the 3 cached data structures (`doneSteps` Set, `idMap` Map, `stepToTaskId` Map). New arrays (e.g., the output of `markTaskRunning` / `markTaskDone`) get a fresh build naturally. WeakMap self-invalidates when the array goes out of scope — no manual lifetime management. For the typical 5-batch/20-task run, ~80% of `buildTaskGraphIndex` calls are now cache hits.
104
+ - **P12 (LOW)** — `mapConcurrent` now short-circuits on `length === 0` (empty array → `[]`) and `length === 1` (single item → `await fn(items[0], 0)`) without setting up the worker pool / Promise.all / counters. Common case for adaptive-plan branching and many task-graph decision points where only one ready task is dispatchable.
105
+ - **P13 (LOW)** — Team-runner heartbeat interval bumped from 30s to 60s. The stale-reconciler threshold is 5min (`staleThresholdMs = 300_000` in crash-recovery.ts), so 60s still leaves 5 ticks of slack before a run is misidentified as stale. Halves heartbeat syscall count (1 write/30s → 1 write/60s) with no behavioral change. Worker heartbeats (1s) and the team-runner interrupt guard are unchanged.
106
+ - **P15** — already addressed by Phase 4's P6 fix (writeProgress artifact dedup moved to O(N) Map).
107
+
108
+ 3 new tests lock in the changes: P14 cache-by-reference identity + cache invalidation across markTaskRunning/markTaskDone; P12 single-item + empty-item fast paths.
109
+
110
+ ### Performance — forward audit quick wins (Phase 4 — P6 + P9 + content-cache)
111
+
112
+ Continuing the Phase 3 work. Addresses 1 MEDIUM-Low finding (P6), 1 MEDIUM finding (P9), plus a content-skip optimization for writeProgress.
113
+
114
+ - **P6 (MEDIUM-LOW)** — `writeProgress()` no longer rebuilds the entire artifact-list with two redundant filter passes (one to remove the old progress entry, one with a quadratic `findIndex` dedup). Now uses a single-pass `Map<path, ArtifactDescriptor>` for O(N) replace-by-path. Also added a content-skip cache (WeakMap keyed on the manifest) — if the rendered progress content is byte-identical to the previous call (rare, but happens during back-to-back applyPolicy + executeTeamRun calls within the same millisecond), we reuse the existing artifact descriptor instead of re-running `writeArtifact` (mkdirSync + resolveRealContainedPath + redactSecrets + atomicWriteFile + readFileSync for hash).
115
+ - **P9 (MEDIUM)** — `computeStablePrefixComponents()` gained a 60s TTL second-level cache keyed on `(cwd, step.task)` that survives `runId` boundaries. Sequential runs in the same session with the same cwd now skip the 3 awaits (`buildWorkspaceTree` + `runRetrievalCycle` + `buildKnowledgeFragment`) on cache hit. The fast path (per-run, `(cwd, step, runId)`) is unchanged so concurrent siblings in the same batch still share with zero FS access. `clearStablePrefixCache()` now also clears the cross-run layer so long-paused sessions re-warm on workspace drift promptly. 2 new tests verify the per-run short-circuit and clearStablePrefixCache behavior.
116
+
117
+ ### Performance — forward audit quick wins (Phase 3 — P5 + P7)
118
+
119
+ Continuing the Phase 2 work. Addresses 1 MEDIUM-Severity and 1 MEDIUM-Low finding from the audit.
120
+
121
+ - **P5 (MEDIUM)** — added a short-TTL (1s) cache for the target-file symlink check in `atomic-write.ts`. The previous code re-ran `lstatSync` on every `atomicWriteFile` call (`isTargetNotSymlink`), even for the common case where the file (manifest.json, tasks.json, events.jsonl inside `.crew/state/`) is a regular file repeatedly. The 1s TTL bounds the TOCTOU swap window to ~1s — same order as the existing `isSymlinkSafeDirCached` (10s). `invalidateSymlinkSafeCache(dir)` now also flushes the per-file cache entries under that dir, so the two caches stay coherent. Set `TARGET_NOT_SYMLINK_TTL_MS = 0` to disable and restore the pre-P5 always-recheck behavior. Estimated ~50% reduction in symlink-check syscalls per write burst.
122
+ - **P7 (MEDIUM)** — migrated 5 fire-and-forget `appendEvent` call sites in `live-session-runtime.ts` to `appendEventFireAndForget`. These events (session_created, bind_extensions_error, prompt_start, prompt_error, prompt_done) had their return value ignored; the sync path acquired the event-log `sleepSync` lock, blocking SIGTERM/AbortSignal handlers during high-frequency prompt-done churn. Sites that pass `appendEvent` AS A CALLBACK to functions expecting `(eventsPath, event) => TeamEvent` (e.g. `appendTranscript`, `terminateLiveAgent`) keep the sync path — the return value is needed there. 3 new tests lock in the cache invariants: cache hit on repeated writes; explicit `invalidateSymlinkSafeCache(dir)` flushes both caches; symlink redirection is still rejected within a write.
123
+
124
+ ### Performance — forward audit quick wins (Phase 2 — top 4 optimizations)
125
+
126
+ Follow-on to the Phase 1 quick wins; addresses the top 3 HIGH-severity findings from the deep audit (`team_20260711095156_8a8a56a3c7dff269`) plus one MEDIUM-Severity security finding from the bundled security review.
127
+
128
+ - **P1 (HIGH)** — `withQueue()` in `task-graph-scheduler.ts` now returns the **same task reference** when the computed queue matches `task.graph.queue`. Eliminates the per-task per-call spread allocation that `refreshTaskGraphQueues` ran on every main-loop iteration. For a 20-task run × 5 batches, ~80% of allocations disappear (only tasks whose queue actually changed get a new object). Behavior unchanged when queues move; downstream code that relies on reference equality now gets the optimization for free.
129
+ - **P2 (HIGH)** — `saveRunManifest` / `saveRunManifestAsync` no longer re-read `tasks.json` from disk after every manifest write. The previous code did `JSON.parse(fs.readFileSync(tasksPath, 'utf-8'))` on every call just to repopulate the cache; ~8+ manifest writes per batch iteration made this the second-hottest I/O path in team-runs. Fix captures `manifestCache.get(stateRoot)` **before** `invalidateRunCache`, reuses the cached tasks array + mtime/size on the write-through repopulation. Cache miss behavior (no prior entry) falls back to `tasks: []` — identical to the pre-fix code.
130
+ - **P3 (HIGH)** — `persistSingleTaskUpdate` reuses the caller-supplied `fallbackTasks` on the **first attempt** instead of calling `loadRunManifestById` again. The caller already obtained the latest tasks via `loadRunManifestById` and handed them in as `fallbackTasks`; the second load was pure waste. The CAS check (`currentMtime !== baseMtime`) catches any concurrent writer — when contention is detected, the retry path **does** call `loadRunManifestById` to pull fresh state. Disk-read cost is now paid only on actual contention, not on the common single-writer happy path. For event-heavy runs (200 events × 2 sources = ~400 calls), this removes ~400 stat+read syscalls.
131
+ - **P10 (LOW)** — `mergeTaskUpdatesPreservingTerminal` replaced its `O(N×M)` `.find()` + `.map()` nested loops with a single-pass `Map<id, task>` index. For a 20-task run × 5-batch merger with ~10 updates per result, this reduces from `O(50×20) = 1000` ops to `O(120)`. Behavior is unchanged: skipped updates (per `shouldMergeTaskUpdate`) still leave the existing task in place, and the reassembly preserves original `base` order so downstream snapshots stay stable.
132
+ - **SEC-M1 (MEDIUM)** — `validateEndpoint` in `otlp-exporter.ts` previously checked only `::1` and `fd`/`fc` IPv6 prefixes. Added defense for `fe80::/10` (link-local), `fec0::/10` (deprecated site-local), `ff00::/8` (multicast), `::` (unspecified), and `::ffff:x.x.x.x` (IPv4-mapped — falls through to IPv4 private rules if checked). Added 5 unit tests covering each new prefix. Closes the SSRF gap flagged in the security review.
133
+
134
+ ### Performance — forward audit quick wins (Phase 1)
135
+
136
+ - **NEW-M1** — memoize stable prompt prefix per `(cwd, step, runId)`. The stable prefix (workspace tree + retrieval + knowledge + skills) was recomputed independently for every task in a parallel batch — identical work for siblings. `renderTaskPrompt` already separates `stablePrefix` from `dynamicSuffix`; pre-warm once per unique cwd in the team-runner batch loop. `clearStablePrefixCache()` in `finally` (matches the existing `flushEventLogBuffer` cleanup pattern) so it does not grow unbounded across runs in a long-lived session. (`08d1a64`)
137
+ - **A1-F7** — guard `collectedJsonEvents` allocation behind `yieldEnabled`. For child-process workers (the common case, `yieldEnabled=false`), the array was allocated and accumulated but only consumed by live-session yield detection — `~10 KB` wasted per task. (`08d1a64`)
138
+ - **A4-F1** — cache the compacted skill body in `CachedSkillMarkdown` at cache-miss time. `compactSkillContent(raw)` ran on every cache hit, wasting `~15%` of skill-rendering time. (`08d1a64`)
139
+
140
+ ### Quality
141
+
142
+ - **A2-F1** — consolidate `cleanupUsage()` into `finally`. Previously duplicated on the success path (`team-runner.ts:719`) and the error path (`:821`), **outside** the `finally` block. A future throw in the success tail or error handler could skip both copies and leak tracked-usage entries. Consolidated into `finally` (alongside `flushEventLogBuffer` and `clearStablePrefixCache`) so cleanup is guaranteed on **every** exit path. DRY + robustness + consistency. Verified safe: nothing in `team-runner` reads usage after the run resolves; the TUI crew widget reads live usage only while a task is running (before this point). (`e1273ab`)
143
+
144
+ ### Audit infrastructure
145
+
146
+ - **Performance audit report** — [`docs/perf/performance-audit-report-2026-07.md`](docs/perf/performance-audit-report-2026-07.md). 26 findings from the forward audit (perf, memory, I/O, concurrency, prompt construction), each with file:line, severity, estimated impact, risk, fix direction, and (post-bench / post-reverse-audit) verdict. Five verdicts recorded: A5-C2 disproven (no same-process contention), A5-C4 backstopped by `signal.aborted` (verified end-to-end), NEW-C6 read necessary (orphan-artifact recovery from write-then-throw workers), NEW-M2 stat is correctness-first (dirStamp would miss in-place edits), A2-F1 → quality fix (cleanupUsage consolidation). Backstop analysis explicitly traced for A5-C4: `handleCancel → abortForegroundRun → controller.abort()` on the EXACT controller from `startForegroundRun` → whose signal is passed as `executeTeamRun({signal})` → threaded to retry loop + `runTeamTask`; `runTeamTask:168` checks `input.signal?.aborted` **before** spawning a worker, so even a 100%-stale disk read cannot produce a zombie.
147
+ - **Reverse audit harness** — `team` action='run' with `review` workflow + direct reviewer subagents, 6 parallel hunts across concurrency / state-machine / resume / cancel / prune / event-log. Each agent received a focused mandate (READ-ONLY, find real correctness bugs, file:line + trigger + severity, already-known exclusions). Agents produced `~25` leads; only those verified firsthand by reading the code were shipped. The signal-backstop analysis (A5-C4) and the orphan-recovery analysis (NEW-C6) both came from reading the call-site code end-to-end — not from agent output alone.
148
+ - **Terminal-persist-blocking benchmark** — [`test/bench/terminal-persist-blocking.bench.ts`](test/bench/terminal-persist-blocking.bench.ts). 4 scenarios (`serialPersist`, `saveManifestLarge`, `singleTerminalBlock`, `spacedBurstPerCall`) under `monitorEventLoopDelay`. Empirically answers the A5-C2 question: `contentionRatio` = spaced per-call p50 / single-block p50. Runs at 0.93–0.98 across 4 runs (consistently ≈1.0) proves **no lock contention** between concurrent task completions; the sync critical sections serialize naturally in the single-threaded event loop and `sleepSync` never fires for same-process workers. The async-lock conversion would relieve contention that does not exist. Captured into `test/bench/results.json`.
149
+
150
+ ### Verification
151
+
152
+ - **Typecheck**: `tsc --noEmit` clean.
153
+ - **Lint / format**: `biome check` clean (1125 files).
154
+ - **Conflict markers / lazy imports**: clean.
155
+ - **Pack dry-run**: clean (`pi-crew-0.9.33.tgz`).
156
+ - **Targeted unit tests**: **145/145 pass** across every changed area (atomic-write, state-store, team-runner, resume, checkpoint, lifecycle, maintenance, process-status, usage-tracker, event-log).
157
+ - **Regression tests proven to fail without the fix and pass with it**: FIND-1 (sync + async paths), CANCEL-3 (cancelled→completed), F3 (completed→failed), F1 (false-green break), EL-1 (sidecar regression via `__test__nextSequence` after `__test__clearSeqCounters` to simulate restart).
158
+ - **End-to-end**: a real `team action='run' goal='...write to /tmp/...'` with the `fast-fix` workflow completed end-to-end in this session (`team_20260711063219_6d1a236ec818315a` — 3/3 tasks completed, manifest `completed`, 3 events with **contiguous seqs 1–3 no duplicates**, sidecar = file max). Every fix exercised on a live workload.
159
+
160
+ ### Honest framing
161
+
162
+ - **False-positive discipline**: agent-reported findings were treated as leads, not facts. The forward audit's A5-C2, A5-C4, NEW-C6, NEW-M2, A2-F1 were each scrutinized — three were WONTFIX (with the reasoning recorded in the audit report), one was a real-but-backstopped correctness concern (A5-C4), one was a quality fix (A2-F1). The reverse audit's ~25 leads filtered to 9 confirmed real bugs. Several agent reports (`R3`, `R4`, `F2`, `F4/F5`, `ART-1`, `FIND-2`, `CANCEL-5/6/7`) remain **unverified** and are deliberately not shipped.
163
+ - **Scope**: no new features. No API surface changes. No breaking changes. All fixes are surgical and behavior-preserving except where they fix incorrect behavior (false-green → blocked; orphan processes → killed; orphan artifacts → preserved; terminal flips → blocked).
164
+
165
+ ---
4
166
 
5
167
  Upgraded the TypeScript toolchain from 5.9.3 to **7.0.2** (the native Go port, a.k.a. Project Corsa / `typescript-go`). This is a dev-toolchain-only change: pi-crew ships source `.ts` loaded at runtime via Node `--experimental-strip-types`, so end users see zero behavioral change. The upgrade affects only `npm run typecheck` and editor type-checking.
6
168