pi-crew 0.11.16 → 0.11.18

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/NOTICE.md +9 -0
  3. package/README.md +1 -0
  4. package/dist/index.mjs +517 -516
  5. package/docs/usage.md +66 -0
  6. package/package.json +1 -1
  7. package/skills/real-test-pi-crew/SKILL.md +19 -3
  8. package/src/config/config-validation.ts +26 -1
  9. package/src/config/config.ts +6 -1
  10. package/src/config/env-allowlist-registry.ts +463 -0
  11. package/src/config/env-vars.ts +13 -0
  12. package/src/config/types.ts +36 -0
  13. package/src/extension/registration/team-tool.ts +7 -1
  14. package/src/extension/registration/terminal-status-wiring.ts +155 -2
  15. package/src/extension/run-index.ts +30 -5
  16. package/src/extension/team-tool/context.ts +6 -0
  17. package/src/extension/team-tool/handle-settings.ts +9 -6
  18. package/src/runtime/async-runner.ts +11 -96
  19. package/src/runtime/broker/crew-broker.ts +12 -0
  20. package/src/runtime/child-pi/child-pi-rpc.ts +61 -8
  21. package/src/runtime/child-pi/child-pi-spawn.ts +5 -31
  22. package/src/runtime/live-session/embedded-tier.ts +994 -0
  23. package/src/runtime/live-session/live-session-runtime.ts +223 -11
  24. package/src/runtime/model/pi-args.ts +30 -2
  25. package/src/runtime/model/runtime-policy.ts +45 -0
  26. package/src/runtime/scheduling/nested-slots.ts +26 -0
  27. package/src/runtime/surface/herdr-provider.ts +58 -7
  28. package/src/runtime/task-runner/child-executor.ts +7 -7
  29. package/src/runtime/task-runner/hybrid-tier.ts +439 -0
  30. package/src/runtime/task-runner/live-executor.ts +53 -0
  31. package/src/runtime/task-runner.ts +159 -51
  32. package/src/schema/config-schema.ts +13 -0
  33. package/src/skills/validate.ts +1 -1
  34. package/src/state/contracts.ts +15 -0
  35. package/src/state/stores/state-store.ts +6 -2
  36. package/src/ui/events-state-source.ts +7 -1
  37. package/src/ui/pi-ui-compat.ts +33 -0
  38. package/src/ui/program-status.ts +74 -0
  39. package/src/ui/syntax-highlight.ts +1 -1
  40. package/src/ui/terminal-status.ts +81 -2
  41. package/src/ui/theme-discovery.ts +38 -2
  42. package/src/ui/tool-progress-formatter.ts +17 -1
  43. package/src/utils/env-allowlist.ts +4 -1
  44. package/src/utils/git.ts +6 -71
  45. package/src/utils/paths.ts +18 -3
package/docs/usage.md CHANGED
@@ -113,6 +113,72 @@ Use scaffold mode only when you want durable prompts/artifacts without launching
113
113
  }
114
114
  ```
115
115
 
116
+ ## Hybrid 2-tier workers: embedded tier (U11, DEFAULT-OFF)
117
+
118
+ `pi-crew` ships a second worker tier — the **embedded tier**: the worker's Pi
119
+ session runs **in-process** (the U4 live-session stack) instead of a child
120
+ process. It trades subprocess crash isolation for ~instant spawn, shared-slot
121
+ steering, and zero orphan risk. The subprocess tier stays the **only** tier
122
+ unless you explicitly opt roles in — with `runtime.embeddedWorkers` absent or
123
+ `enabled: false`, behavior is byte-identical to the default (no imports, no
124
+ events, no budget reads).
125
+
126
+ Opt in per role (roles NOT listed always run as subprocess):
127
+
128
+ ```json
129
+ {
130
+ "action": "run",
131
+ "team": "default",
132
+ "goal": "Research + implement",
133
+ "config": {
134
+ "runtime": {
135
+ "embeddedWorkers": {
136
+ "enabled": true,
137
+ "roles": ["explorer", "verifier"],
138
+ "maxConcurrent": 2,
139
+ "faultThreshold": 2
140
+ }
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ - `roles` — roles allowed to run embedded. `isolationPolicy.isolatedRoles`
147
+ and the nesting guard (`PI_CREW_DEPTH > 0`) ALWAYS beat the opt-in: a
148
+ crash-isolated or nested worker never leaves the subprocess tier. The
149
+ legacy `runtime.mode: "live-session"` path is unaffected by this block.
150
+ - `maxConcurrent` — ceiling on concurrently in-flight embedded sessions
151
+ (default `max(1, floor(worker-cap/2))`). At the ceiling, new tasks fail-fast
152
+ back to the subprocess tier immediately (never queue on the ceiling).
153
+ - `faultThreshold` — per-task budget before that task is permanently rerouted
154
+ to subprocess (default `2`). Aborts/cancellations never count.
155
+
156
+ **Safety model** (all enforced only on the embedded path, fail-closed):
157
+
158
+ - **Builtin-tools-only** — no MCP/extension tools, verified after apply;
159
+ degradation fails the task instead of running unsandboxed.
160
+ - **Provider `timeoutMs`** — every model HTTP call is clamped so a hung
161
+ provider cannot hold a worker slot; concurrent sessions share one clamped
162
+ registration restored exactly when the last one leaves.
163
+ - **Turn-count watchdog** — a turn-spinning session that ignores the soft
164
+ wrap-up is disposed after a bounded grace.
165
+ - **Shared slot budget** — embedded and subprocess workers draw from the SAME
166
+ global worker cap; there is no concurrency bypass for the embedded tier.
167
+ - **Blast-radius guard** — a process-level `uncaughtException` guard
168
+ quarantines (dispose + unregister) every in-flight embedded session and
169
+ keeps the host alive, bounded by a circuit breaker (20 quarantines → normal
170
+ crash behavior); memory metrics are emitted rate-limited and ring-capped.
171
+ - **Global-state asserts** — factory-surface and services-cache post-condition
172
+ checks fail/fault loudly on drift.
173
+
174
+ Automatic fallback: after `faultThreshold` counted embedded faults (fault kinds
175
+ like `session-error`, `hung-tool`, `turn-watchdog`), the task reroutes to the
176
+ subprocess tier for all remaining attempts — sticky across retries and
177
+ recovery (persisted in `task.diagnostics.embeddedTier`). Routing decisions and
178
+ fallbacks are visible as `tier.decision` / `tier.fault` / `tier.fallback`
179
+ structured events; embedded-session telemetry as `live-session.embedded_*`
180
+ events.
181
+
116
182
  ## Async run
117
183
 
118
184
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.16",
3
+ "version": "0.11.18",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -55,6 +55,12 @@ triggers:
55
55
  - "output contract"
56
56
  - "loop guard"
57
57
  - "post-init skill check"
58
+ - "embedded tier"
59
+ - "hybrid worker"
60
+ - "tier.decision"
61
+ - "embeddedWorkers"
62
+ - "stale bundle"
63
+ - "unexpected property"
58
64
  - "resource contract"
59
65
  - "sigterm"
60
66
  - "silent bash"
@@ -125,12 +131,13 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
125
131
  | `workflows/*.workflow.md` | T7 |
126
132
  | `.github/workflows/ci.yml` | T11e |
127
133
  | `scripts/wc-gate.mjs` | T11b |
128
- | `scripts/test-runner.mjs` (the runner itself) | T1 (exercises it) + `test/unit/scripts/test-runner-exit.test.ts` (21 tests: exit codes, cancelled-guard red-green, missing-file pre-check, batch shard empirical) + ONE batch-mode run (`PI_CREW_TEST_BATCH_SIZE=2 node scripts/test-runner.mjs --shard=0/1 <3 small files>`). Dev script — NOT bundled, no T3/T4 needed |
129
- | `scripts/check-lazy-imports.mjs` | Run the gate on a clean tree (`node scripts/check-lazy-imports.mjs` exit 0) + re-probe the pattern battery vs the KNOWN GAPS comment (round-3 lesson: a skip-rule fix can OPEN a new false-skip hole — probe both directions). NOT bundled |
134
+ | `scripts/test-runner.mjs` (the runner itself) | T1 (exercises it) + `test/unit/scripts/test-runner-exit.test.ts` (21 tests: exit codes, cancelled-guard red-green, missing-file pre-check, batch shard empirical) + ONE batch-mode run (`PI_CREW_TEST_BATCH_SIZE=2 node scripts/test-runner.mjs --shard=0/1 <3 small files>`). **Home isolation per-suite từ 2026-10-11** (set CẢ `PI_CREW_HOME` + `PI_TEAMS_HOME` — mirror pair, đặt 1 tên là defeatable; sweep prefix `pi-crew-` cover orphan `pi-crew-test-home-*`; opt-out `PI_CREW_TEST_NO_HOME_ISOLATION=1`) → khi đụng isolation: leak detector = đếm run-dirs `~/.pi/agent/extensions/pi-crew/state/runs/` before/after một suite, KHÔNG được tăng. Dev script — NOT bundled, no T3/T4 needed |
135
+ | `scripts/check-lazy-imports.mjs` | Run the gate on a clean tree (`node scripts/check-lazy-imports.mjs` exit 0) + re-probe the pattern battery vs the KNOWN GAPS comment (round-3 lesson: a skip-rule fix can OPEN a new false-skip hole — probe both directions; fixture suite `test/unit/scripts/check-lazy-imports-gate.test.ts` 4 tests probe cả 2 hướng). **AST-based từ Δ2 (2026-10-11, esbuild strip + acorn)**: per-entry budget `{maxFiles}` là RATCHET — bump chỉ khi chủ ý + justify trong commit; hosted-git-info allowlist ĐÃ xóa (2026-10-12, dead require dropped — gate strict trở lại). NOT bundled |
130
136
  | `src/runtime/verification/verify-gate.ts` + task-runner gate mount (U13) | T7 smoke ×2 — (a) goal whose checks PASS → run must reach `completed` (NOT `blocked`) + `task.verifier_pre_gate` event with `skipVerifier:true` + verifier result artifact carries gate summary; (b) goal whose checks FAIL (e.g. `false` command) → LLM verifier spawns WITH gate context. **Guards the U13×effectiveness interplay** (live-caught 2026-10-10: green-gated verifier = no usage/transcript by design → effectiveness guard false-blocked the run → sync calls hung forever; fixed by counting `diagnostics.verifyGate` as observable work — `effectiveness-guard.test.ts` pins it) |
131
137
  | `PI_CREW_STATE_BACKEND=sqlite` (U8 state backend) | Unit: `sqlite-run-state.test.ts` + `state-store-sqlite-parity.test.ts` (incl. coalesced-mirror pin). LIVE: one fast-fix run with the env set → assert `state.sqlite` exists, **sqlite tasks reach terminal status** (live-caught 2026-10-10: `saveRunTasksCoalesced` hot path skipped the db — manifest+tasks.json completed while sqlite stuck at createRun `queued`), events.jsonl still written (audit spine), JSON mirrors present |
132
138
  | `PI_CREW_CHILD_PI_MODE=rpc` / `PI_CREW_CHILD_PI_POOL` (U14 transport) | Unit: `child-pi-rpc-mode.test.ts` (18) + `owned-process-u15.test.ts` (11). LIVE: one async run with env set → workers' status.json carry rpc transport markers, run green, `pgrep -f "mode rpc"` clean after completion (no orphan rpc processes), json default unaffected |
133
139
  | `src/runtime/live-session/**` shared-services rework (U4) | Unit: `live-session-u4` suite (10 — init<50ms via `session_created.elapsedMs`, recursion assert, D4 planning-window replay, hung-tool drop). LIVE: T8 md5 + one fast-fix run (live-session path opt-in) — session_created.elapsedMs < 50ms in events |
140
+ | `runtime.embeddedWorkers` config / hybrid 2-tier worker (`src/runtime/task-runner/hybrid-tier.ts` + `src/runtime/live-session/embedded-tier.ts`, U11 — DEFAULT-OFF) | Unit: `test/unit/runtime/task-runner/hybrid-tier.test.ts` (19) + `test/unit/runtime/live-session/embedded-tier.test.ts` (17, gồm 2 regression P1 provider-wrapper leak) + no-op pin (block absent ⇒ routing byte-identical, không import/events/budget reads). LIVE probe (chỉ khi bật): project `.crew/config.json` trong scratch **GIT** dir (không phải bare /tmp) + fresh `pi -p` → run nhỏ → assert `tier.decision {route:"embedded"}` cho role opted-in, `live-session.session_created.elapsedMs < 50ms` (spot-sample 9ms), child-pi trail cho role còn lại, run green. **STALE-BUNDLE SIGNATURE (live-caught 2026-10-11)**: config key MỚI + session đang serve dist CŨ → `config.warning "/runtime/embeddedWorkers: Unexpected property"` (schema reject, key bị drop âm thầm) — `npm run build:bundle` + cold-start TRƯỚC khi kết luận "config không được nhận". Fallback 2-faults → `tier.fallback` + subprocess hoàn thành task (unit-pinned `[u11-3b]`/`[u11-2c]`) |
134
141
  | `src/runtime/effectiveness.ts` / guard semantics | `effectiveness-guard.test.ts` + `effectiveness-cov.test.ts` + T7 smoke (sync) — **a completed run must never end non-terminal `blocked` when every task has evidence**; sync T7 hang = guard/gate interplay regression smell |
135
142
  | `dist/` (bundle) | T3 → T4 → T8 → T11j |
136
143
  | `src/runtime/rpc/**` (worker transport seam — RPC vẫn EXPERIMENTAL) | Pinned suites `test/unit/runtime/rpc/rpc-worker.test.ts` (10) + `test/unit/runtime/run-worker-rpc-seam.test.ts` (8, gồm DR1/DR5 exit-race) + **T7 smoke với `PI_CREW_WORKER_TRANSPORT=rpc`** khi đụng transport wiring; stdio fallback phải giữ structured warn |
@@ -138,6 +145,12 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
138
145
  | `src/runtime/child-pi/pi-version.ts` / version-floor wiring (U9) | `test/unit/runtime/child-pi/pi-version.test.ts` (11) + `test/unit/runtime/model/pi-args-version-floor.test.ts` (7) + `test/unit/runtime/child-pi/pi-version-floor-wiring.test.ts` (2) |
139
146
  | `.githooks/` + `scripts/install-hooks.mjs` | `bash -n .githooks/pre-push` + MỘT push thật quan sát hook (main → full `test:unit` + fast gates; ref khác → fast gates; kỳ vọng dòng `[pre-push] gates green.`) |
140
147
  | `src/config/**` (env-vars keys MỚI) | `test/unit/config/config-schema-sync.test.ts` + `npm run check:env-vars` (mọi `PI_CREW_*` mới phải registered + routed qua `getCrewEnv`) — không cần live tier |
148
+ | `src/config/env-allowlist-registry.ts` / thêm `PI_CREW_*` control var MỚI (C-1, 0.11.17) | `test/unit/config/env-allowlist-registry.test.ts` + `npm run check:env-vars` — registry **fail-fast AT IMPORT**: var mới thiếu forwarding decision (allowlist / explicit channel / child-write / host-only / gap / test-only) → throw lúc load. Nhận dạng: extension không load + stack chỉ env-allowlist-registry = guard chủ đích, KHÔNG phải bug; thêm decision rồi chạy lại. Content pin: allowlist output byte-identical trừ var thêm có chủ ý |
149
+ | `src/runtime/child-pi/child-pi-rpc.ts` aborted-settle marking (L2-1, 0.11.17) | Pinned `child-pi-rpc-abort-settle.test.ts` (3) + `assemble-attempt-outcome.test.ts` (10, gồm regression MAJOR-1: host-aborted shape exit-0/no-error phải classify `cancelled`). LIVE (chỉ host ≥1.1.0): cancel run giữa chừng → attempt outcome `crashClass:"cancelled"` (KHÔNG phải completed) + rpc pool KHÔNG re-register worker đó; host 1.0.4 byte-identical legacy |
150
+ | `PI_CREW_TOOLS_MODIFIERS=1` (+tool/-tool spawn, L2-3, 0.11.17) | Unit: pi-args modifier suites (strict-parser). LIVE probe CHỈ khi env bật — default OFF vì additive syntax MỞ RỘNG tool surface (semantics đổi từ least-privilege replacement allowlist — CHANGELOG 0.11.17 decision); host ≥1.1.0 → argv `--tools +a,…`, host dưới floor → legacy allowlist |
151
+ | `src/ui/terminal-status.ts` + `src/ui/program-status.ts` (OSC 7501, B-1, 0.11.17) | T1 → (T5|T6) → T13 như mọi `src/ui/` + probe 7501: tmux session thật, park run vào plan-approval/mailbox-ask → `capture-pane -e` phải thấy `\x1b]7501;blocked;question…` (vendored wire format — KHÔNG import pi-tui; kênh best-effort no-throw, terminal bỏ qua im lặng là ĐÚNG) |
152
+ | `src/ui/theme-discovery.ts` delegate (B-4, 0.11.17) | `test/unit/extension/theme-settings-routing.test.ts` (11: delegate-when-ctx-nonempty / fs-fallback khi ctx rỗng-RPC `[]` / degenerate) — KHÔNG cần live tier |
153
+ | `src/config/config.ts` / `src/state/stores/state-store.ts` junk-path guard (0.11.17) | `test/unit/utils/path-literal-guard.test.ts` — `resolveHomeDir`/`createRunPaths` phải throw trên literal `"undefined"`/`"null"` (từng sinh thư mục rác `pi-crew/undefined/.pi`) |
141
154
  | bất kỳ đường dẫn trên, trước khi commit | gates đầy đủ: critical + unit + tsc + biome + md5 |
142
155
 
143
156
  **When to use**: after any change to `src/runtime/broker/*.ts` (broker + tokens + issuer), `src/ui/`, `src/config/` (incl. `src/config/migration-validator.ts`), `src/extension/registration/lifecycle-handlers.ts`, `src/runtime/child-pi/*.ts` (worker spawn/kill/steering), `src/runtime/mcp-proxy.ts` + `src/runtime/model/pi-args.ts` (worker argv build + MCP role-gate — G2/G3, SDD-2), `src/runtime/async-runner.ts` + `src/runtime/background-runner.ts` (detached spawn + allowlist + entry-guarded `executeBackgroundRun` core — async dispatch, xem quy tắc tier ở decision table), `src/runtime/heartbeat/*` + `src/ui/heartbeat-aggregator.ts` (ambient heartbeat — guest `gc-*` skip), `src/runtime/surface/*.ts` (MuxSurface providers, degrade, launch script), `src/prompt/*.ts` (worker-side tools: ask / message / delegate / surface-worker recorder), `src/runtime/goal-workflow/plan-templates.ts`, `src/runtime/team-runner.ts` or `src/runtime/task-runner/**` (scheduler / execution — Tier 7 smoke), `src/state/**` (durable state — Tier 7 + 9a events/status + **Tier 11a read-your-writes**), `src/utils/paths.ts` + `src/utils/project-markers.ts` + `src/runtime/stale-reconciler.ts` + `src/extension/team-tool/health-monitor.ts` (run-state layout + symlink defense — pinned suites listed in the 2026-09-20 update), `src/runtime/live-session/**` + `src/runtime/custom-tools/*` (live-session mode + worker custom tools), `src/schema/team-tool-schema.ts` (or any `Type.Unsafe({...})` schema definition), `src/extension/registration/team-tool.ts`, `workflows/*.workflow.md`, `.github/workflows/*.yml` (CI env — Tier 11e), `scripts/wc-gate.mjs` (Tier 11b), or before any commit touching these paths. Schema changes additionally require Tier 9 (feature battery) because the team tool's TypeBox schema is validated by pi-ai BEFORE the handler runs — a too-strict or malformed schema breaks every action silently. Surface changes additionally require Tier 10 (surface-mode battery) because surface is fail-closed: every failure degrades to headless and the run still goes green — only pane-level evidence proves the panes engaged. Resource `.md` changes (agent bodies/frontmatter, skill metadata, discovery, frontmatter parsing) additionally require **Tier 12** (resource-contract battery) because the agent/team/workflow frontmatter parser is line-based, not YAML — a folded scalar parses as `">"` for every consumer while all other tiers stay green.
@@ -157,11 +170,14 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
157
170
  > **2026-10-01 update (SDD-2 W-B — G2 MCP role-gate + G3 spill-always + Phase D bump 0.99.1, commits `66f37875`→`f0b343ed`, execution record §13 `pi-crew-sdd-2026-09-30-buoi1.md`)**: (1) **G2 flips full-loadout sanity semantics** — MCP presence in workers is now ROLE- and PATH-dependent: the live-session SDK path strips MCP (`DefaultResourceLoader({ extensionsOverride: stripMcpExtensions })`) cho read-only roles (`mcpPermittedForRole` — explorer/reviewer/security-reviewer/analyst/critic/planner + unknown default-deny); a read-only worker WITHOUT MCP is CORRECT, a write-role worker without MCP is a regression; nếu strip không arm được (SDK hết export `DefaultResourceLoader`…) → fail-open + warning event `task.mcp_enforcement_degraded` (registry 168 → 169, `check:event-types --enforce` clean). CLI child-process path CHƯA cover (WI-4 defer: `~/.pi/agent/mcp.json` trống + `-ne` giết provider `oc-go` — read-only roles vẫn discover adapter MCP trên đường đó). (2) **G3 spill-always**: MỌI task text qua `task.md` 0600 — argv worker KHÔNG bao giờ chứa task text; probe đọc argv phải expect `@<temp>/task.md`, task ngắn mất prefix `Task: `; `TASK_ARG_LIMIT` đã xóa. (3) Phase D bump `@earendil-works/*` → 0.99.x: SDK bỏ option `enableMCP`, tool seam cần `ExtensionToolContext` (export mới `src/extension/pi-api.ts`). Decision table thêm row `mcp-proxy.ts`/`model/pi-args.ts`; D5 loadout line refs dời `:283-330` → `:280-336`.
158
171
  > **2026-10-07 update (0.11.6 → 0.11.7/0.11.8-pre stack, GH#61/#62 + pi 1.0.4 adoption + RPC hardening, HEAD `11e717f5`)**: (1) **GH#61 (0.11.6)** — directory `pi.extensions` entries được normalize sang entry-file identity (`resolveExtensionDirEntry`): child worker không còn double-load directory extension (`Tool "…" conflicts … exit 1` — CHANGELOG 0.11.6 block, `CHANGELOG.md:207`). (2) **pi 1.0.4 adoption** — devDeps `@earendil-works/*` ^1.0.4 (`package.json:153-156`; peerDeps `:136-139` giữ `*`), host pi 1.0.4. (3) **U2-lite MCP parity cut** — parity spawns fold `mcp__*` vào disallow list trên MỌI host (`pi-args.ts:414`) + `--no-mcp` khi host ≥ `PI_NO_MCP_FLOOR="1.0.4"` (`pi-version.ts:76`, gate `pi-args.ts:430`); hermetic spawns MCP-clean qua `--no-extensions` — probe loadout: worker KHÔNG thấy `mcp__*` là ĐÚNG trên cả parity lẫn hermetic path (verdict `docs/reviews/pi-1.0.4-upgrade-notes-2026-10-06.md`). (4) **RPC transport wired (W7)** — `PI_CREW_WORKER_TRANSPORT=rpc` chọn RPC transport tại run-worker seam (`runWorker`, `src/runtime/run-worker.ts:138`; env names `src/runtime/rpc/rpc-worker.ts:43-49`), precedence **env > `runtime.workerTransport` config > stdio**, invalid → stdio (`config-schema.ts:157`); probe recipe = T7 smoke với `PI_CREW_WORKER_TRANSPORT=rpc`; DR1/DR5 exit-race hardening + `PI_CREW_RPC_TURN_TIMEOUT_MS` (default 600_000; dialog policy `PI_CREW_RPC_DIALOG_ANSWER` default `cancel`) — suite `run-worker-rpc-seam.test.ts` (8) có 3 scenario MỚI: DR1 exit-race `:223`, DR5 started-then-fail KHÔNG fallback `:266`, DR1 turn-timeout `:298` (+ truth table `:328`); pin kèm `test/unit/runtime/rpc/rpc-worker.test.ts` (10). RPC vẫn EXPERIMENTAL — fail trước agent output → stdio fallback (structured warn). (5) **Classifier seam DORMANT (P2-1)** — default OFF (`config-schema.ts:95`; `resolveClassifierEnabled`: env `PI_CREW_CLASSIFIER_ENABLED` > config > FALSE, `classifier-service.ts:158`); battery KHÔNG dính trừ khi set `PI_CREW_CLASSIFIER_ENABLED=1` (+ `PI_CREW_CLASSIFIER_MODEL`, default `opencode/jev-1.13-free`) — unit-only (`classifier-service.test.ts` 14 + `child-executor-retry-triage.test.ts` 6), KHÔNG live tier. (6) **Pre-push hook (D3)** — `.githooks/pre-push` qua `git config core.hooksPath .githooks` (wired `package.json` `prepare` → `scripts/install-hooks.mjs`): MỌI push chạy `check:conflict-markers` + typecheck; push `refs/heads/main` TỰ chạy FULL `test:unit` (~13-14 phút) rồi mới in `[pre-push] gates green.`; bypass `git push --no-verify` CHỈ khẩn cấp (CI vẫn gate main). (7) **GH#62 foreground completion** (`62c9ed0d`→`51adb1e1`→`3ed0067e`→`c956b2d4`→`af462d7b`) — foreground-run completion path sống sót qua spread-copy contexts; suite `test/unit/extension/registration/foreground-run-completion.test.ts` (4 tests). (8) **hermeticWorkers DEFAULT ON** (`config-schema.ts:89`; env `PI_CREW_HERMETIC_WORKERS`; `resolveHermeticWorkers` `pi-args.ts:149` — `explicit ?? true`). (9) **U9 version-floor wiring** — suites `test/unit/runtime/child-pi/pi-version.test.ts` (11) + `test/unit/runtime/model/pi-args-version-floor.test.ts` (7) + `test/unit/runtime/child-pi/pi-version-floor-wiring.test.ts` (2). (10) **RT-11 lesson (`48cc088d`)** — static spawn-site scan (`child-pi-spawn-registration.test.ts:33`) đếm CẢ prose trong comment (`spawn (`); khi dash vì comment → **reword comment, KHÔNG relax test**. Suite anchors @ 2026-10-07: unit **8711 tests / 8707 pass / 1 flaky-under-load / 3 skipped** (968 files, ~13 min; flaky = `team-tool-resume-liveness` F1 liveness race — 6/6 pass isolated ×3, KHÔNG phải regression), integration **139 / 135 pass / 4 skipped** (exit 0 — CHỈ khi scrub ĐỦ `PI_CREW_*`/`PI_TEAMS_*`, xem Failure-symptoms row MAX_DEPTH), `test:critical` **120** (~19-20s), bundle **1620.1 KB** (1,658,985 B) md5 `20544cf9ea436c69b0b434462def91b9` (`--committed-hash` OK), event registry **176 registered / 141 emitted** (`check:event-types --enforce` clean), devDeps `@earendil-works/*` ^1.0.4 / host pi **1.0.4**.
159
172
  > **2026-10-08 update (L-wave UI instability review + L10 plan flicker + P0 completion crash, commits `28e0d58d`+`c068fc4a`+`c89c759a`, report `docs/real-test/reports/real-test-2026-10-07-ui-instability-review.md` + fix-wave addenda)**: (1) **P0 — completion throw giết cả pi**: `/team-transcript` popup race làm input dính `/<cmd>` suffix vào runId token → `assertSafePathId` throw bên trong `CombinedAutocompleteProvider.getSuggestions` (pi core KHÔNG catch lỗi extension) → uncaught_exception chết cả process. Fix: MỌI completion provider (`suggestTaskIds`, `suggestRunIds`…) try/catch → null; pin test + mutation. **Evidence source mới: `~/.pi/agent/crashes.json`** — khi pi/tmux chết bất ngờ giữa probe, đọc file này TRƯỚC khi đoán (có kind/message/stack qua dist). Decision table có row mới cho command-completions. (2) **L10 — plan card flicker**: tasks variant không được paint spinner động (comment thiết kế của chính nó); signature phải strip `:spin=`. Dock giữ đúng MỘT dòng spinner (chủ ý). (3) **Probe-hygiene lessons** (đều tốn điều tra giả session này): tmux `new-session -t <sess>` tạo GROUPED session (cùng process — vô dụng cho bystander probe; phải `new-session -d -s <tên> "cd … && exec pi"`); `pkill -f <pattern>` từ bash của assistant TỰ MATCH command line của chính nó (chuỗi literal nằm trong args/heredoc — bracket-trick không đủ; kill bằng PID hoặc tên script riêng); so sánh frame theo row-index bị transcript-scroll làm nhiễu — so BLOCK theo marker `┏ NAME ▸`…`┗`, và dock có HAI dạng render (`┏ CREW ▸` mở rộng / `┃ CREW ▸` thu gọn — marker phải match cả hai, từng gây báo động giả "dock biến mất"); dispatch sync run khi có run khác đang chạy → assistant có thể join/wait run cũ thay vì tạo run mới (luôn verify runId mới trong `.crew/state/runs/` trước khi probe). (4) **Load-flake rule**: `test:critical` đỏ 1 suite lạ + treo isolation dưới load 82+ (build song song của user) = starvation, KHÔNG phải regression — check `uptime` TRƯỚC, re-run khi máy êm (session này: symlink-steering treo dưới load 82, 120/120 xanh khi load 0.8). Suite anchors @ 2026-10-08: `test:critical` **120**, bundle **1622.6 KB** md5 `4f76457497bd20779e298cee4d8d982e`.
173
+ > **2026-10-11 update (Wave A — pi-1.1.0 adoption + env registry, v0.11.16→0.11.17, commits `7fbe592f`..`cd09ca8a`, 12 commits; specs `pi-crew-upgrade-research-round2.md` + `pi-crew-upgrade-research-round3-deep.md`)**: (1) **Wiring-gap class MỚI — 3 MAJOR bắt bởi C-8 review round, KHÔNG phải bởi battery**: code đúng + unit test gọi TRỰC TIẾP hàm mới vẫn dormant — B-3 durationMs chưa producer nào truyền, B-4 getAllThemes chưa call-site nào truyền ctx, L2-1 marking chết ở tầng `exitStatus.cancelled` (consumer `evidenceStatusFor`/run-coalesced vẫn thấy completed). Fix-round `b19e22e3` plumb end-to-end + regression pin **producer→consumer thật** (counterfactual clock +600s chứng minh producer thắng self-timing; theme test gọi qua entry symbol thật). **Quy tắc**: optional-param API mới → review PHẢI tìm call-site truyền thật (C-8 rule 5); test phải đi qua đường invoke thật, không gọi hàm trực tiếp. (2) **R3-1 strip-types break invisible to CI** — TS parameter property (`constructor(readonly …)`) giết MỌI strip-only load (bench + bench-cold-start gãy ~24h) mà script `ci` không chạy bench → không ai thấy. Rule: src/ KHÔNG dùng parameter properties; đụng bench infra → chạy ≥1 bench file sau sửa. (3) **R3-2 bench query calibration** — b13 warm FAIL `files=0` KHÔNG phải regression retrieval: query cũ rơi junk-band (best 0.42 < floor 0.5, grazer band 0.45-0.47); gold query mới 0.60/0.56, 28 files ≥ floor. Lesson: pass-criteria `files.length>0` cần query ĐÃ CALIBRATE — nếu không kết luận sai hướng (đã suýt kê floor). (4) **Env registry fail-fast (C-1)** — `src/config/env-allowlist-registry.ts` sinh 2 allowlist từ 1 decision table; var mới quên decision → throw at import (xem row decision table mới). (5) **pi 1.1.0 markers** — `agent_settled.aborted` detect `record.aborted === true` (chặt hơn `'aborted' in record`); host-aborted worker giờ `crashClass:"cancelled"` + evict khỏi rpc pool — trên host 1.1.0 đừng debug "worker exit 0 completed lạ" nữa, check aborted trước; OSC 7501 vendored từ pi-tui v1.1.0 (MIT, NOTICE.md attribution; peer không floor 1.1.0 + PROGRAM_STATUS_QUERY không re-export → KHÔNG import host). (6) **Baseline bench** — `test/bench/baseline.json` giờ linux/node v22.23.1 (2026-10-11; capture dưới residual external load — annotate khi so số); bản win32/v24 2026-05-14 retire. Suite anchors @ 2026-10-11: unit **9011 tests / 9008 pass / 0 fail / 3 skipped** (868s), integration **144 / 140 pass / 0 fail / 4 skipped** (163s), `test:critical` **120** (~20s), bundle **1698.7 KB** (1,738,680 B) md5 `b7194c0cbc5eee88e9add4b72685d951` (`--committed-hash` OK); npm latest 0.11.16 — 0.11.17 local CHƯA publish.
160
174
  > **2026-09-21 update (F-L2 — steer boot-window loss, found by the live feature battery)**: a steer written to `<artifactsRoot>/steering/<taskId>.jsonl` while the worker extension is still LOADING was silently lost forever — `registerPiTeamsPromptRuntime` starts the file poll at load time, pi's loader gates action methods until bind (`sendMessage: notInitialized` THROWS), and the old poll advanced `lastOffset` to `stat.size` BEFORE the line loop while its per-line catch swallowed the throw as "malformed line" (run `team_20260921035840`, root-caused by watcher-driven bisection probes R7-R9: +0.65s post-boot = lost, +2.5s+ = delivered). Fix (`src/prompt/prompt-runtime.ts`): walk the buffer by byte offset, advance only past terminal verdicts (delivered/non-steer/rejected/malformed), rewind to the failing line on a DELIVERY failure, `createSeenSteerIdSet.unmark()` releases the dedup id so the retry is not skipped. Regression suite `test/unit/prompt/prompt-runtime-steer-boot-window.test.ts` (4 tests) — mutation-checked both layers (offset-revert → 4 fail; unmark-removal → exactly the id-bearing test fails). Note: `dist/index.mjs` is UNCHANGED by this fix — child workers load prompt-runtime from the SOURCE path (`pi-args.ts PROMPT_RUNTIME_EXTENSION_PATH`), so the fix is live without a bundle bump.
161
175
  >
162
176
  > **F-L2 live re-verification (post-restart, 2026-09-21 afternoon)** — two independent proofs: (a) **lab**: pre-write the steer, then `PI_CREW_STEERING_FILE=<file> pi --extension <repo>/src/prompt/prompt-runtime.ts -p '<long task>'` — stderr shows `[pi-crew:prompt-runtime.steer-delivery-failed] Extension runtime not initialized…` ×2 (pre-bind ticks now LOGGED instead of swallowed) then the post-bind tick delivers and the model complies; (b) **real team run**: watcher writes at **+0.51s** after the steering dir appears (the previously-lost window) → worker session log gains the `custom_message` and the probe token lands in `results/<taskId>.txt`. Suite anchors @ 2026-09-21 (post-fix): unit **8006 tests / 8003 pass / 0 fail / 3 skipped** (875 files; 1088s under load), integration **134 / 130 pass / 4 skipped**, `test:critical` **116**, bundle **3360.1 KB** md5 `2f45df91ed0580ee5bb5b9b57a8a8c3f` (UNCHANGED — see the source-path note above).
163
177
  >
164
178
  > **Steering channel filename is RUN-KIND-DEPENDENT — the #1 way to fake a failure** (cost this session a full false-negative investigation): **team workflow runs** poll `<artifactsRoot>/steering/<taskId>.jsonl` (`child-executor.ts:562` → env `PI_CREW_STEERING_FILE` → `prompt-runtime.ts:855`), e.g. `01_assess.jsonl`, `adaptive-01-executor.jsonl`; **crew_agent / direct subagent runs** use the agent-slot name `<slot>-agent.jsonl` (e.g. `01_01-agent.jsonl`). A watcher writing `01_01-agent.jsonl` into a team run's steering dir writes a file **no component reads** (0-byte `<taskId>.jsonl` sits untouched, the phantom file accumulates bytes) — it looks exactly like the boot-window loss but is a probe bug. Always derive the name from the run kind, or write every `*.jsonl` present in the dir (see Tier 9g).
179
+ >
180
+ > **2026-10-12 update (test-state leak fix + U11 hybrid worker + Δ-wave, commits `468fe609`..`a56129ec`, 8 commits)**: (1) **Unit-test state leak vào user root (fix 2 tầng)** — fixture chạy từ bare `/tmp` cwd (không repo marker) đẩy run-state vào `~/.pi/agent/extensions/pi-crew/` qua no-repo fallback của `scopeBaseRoot()`; async-notifier mỗi live session toast các fixture failure thành `Error: pi-crew run failed: team_… (inline-seam-fixture/none)` (104 run-dir rò từ tháng 6, dọn 2026-10-11). Tầng 1: runner isolation per-suite (`PI_CREW_HOME` + `PI_TEAMS_HOME` — CẢ HAI, mirror pair 1-tên defeatable); tầng 2: run-index temp-cwd guard (non-temp root × TERMINAL × bare-temp cwd không marker ⇒ ẩn khỏi listing; by-ID vẫn resolve). Probe rule: leak detector đếm run-dirs user-root before/after suite. (2) **U11 hybrid 2-tier worker (DEFAULT-OFF)** — embedded tier trên U4 stack, shared slot budget 2 tier, fallback 2-faults, events `tier.decision/fault/fallback`; LIVE acceptance `team_20261011150612`: embedded `session_created.elapsedMs` **9ms** vs subprocess spawn **317ms** (~35-60×); row decision-table mới ở trên kèm STALE-BUNDLE SIGNATURE. (3) **Δ-wave benches + gates**: `check-lazy-imports` AST hóa (budget ratchet 523, fixture 4 tests); **b14** run-index scan (readdir vô hại 1.4µs/dir — per-entry work 0.3ms/dir mới là chi phí; notifier recent-40 an toàn mọi scale; F4: scan-cache bucket TTL non-sliding + `set()` reset bucket rỗng — scan >1s tự roll bucket giữa chừng, cần decision record nếu bucket nào phục vụ scan >1s); **b15** broker scan-cost (Δ3 verdict NO-GO có bằng chứng: N≤11 tasks/run thực tế, scan 0.006-0.05ms/call vs I/O floor 8.1ms — candidate indexes parked tới khi run thật >50 tasks); `hosted-git-info` dead-require dropped (KHÔNG resolve ở đâu cả — nested `node_modules/@peer/...` không nằm resolution path của pi-crew; generic parser là de-facto path 100% thời gian). (4) **U12 track** — 7 blacklist items mới từ upstream `cdaf6c227`, triggers 1/4 → vẫn track-only (`docs/reviews/pi-upstream-tracking-u12.md`). Suite anchors @ 2026-10-12 (`a56129ec`): unit **9063 tests / 9060 pass / 0 fail / 3 skipped / 0 cancelled**, integration **144 / 140 pass / 0 fail / 4 skipped** (EXIT=0, env scrub), `test:critical` **120**, bundle **1,758,732 B (1717.5 KB)** md5 `472cb93254c69f7302b99e47269e02ea`, event registry **182 registered / 147 emitted** (`tier.*` registered; DRIFT 3 = mailbox cũ đã biết, owner broker lane).
165
181
 
166
182
  ## Core principle: disk ≠ live Pi
167
183
 
@@ -345,7 +361,7 @@ Compare the printed md5 against what the user's Pi session loaded. If they diffe
345
361
  | Bundle builder | `scripts/build-bundle.mjs` (esbuild-based, bundles `index.bundle.ts` → `dist/index.mjs`) |
346
362
  | Bundle resolution rule | `index.ts:1-25` (entrypoint docstring); also `scripts/build-bundle.mjs:14-20` (entrypoint preference); **symlink is live for source files but the bundled `dist/index.mjs` is loaded** |
347
363
  | Postinstall hook | `scripts/postinstall.mjs:43` — best-effort bundle rebuild; falls back to strip-types if esbuild missing |
348
- | Bundle md5 anchors | `1cc4d55e18add7b9a036c569143320b6` (Phase-4 flip, ~2.78 MB) → `16e29d053bd370e24f40df147dadcb79` (v0.9.66, 2026-08-11) → `9b557ac106b82e1ee33d39dd0d6c7dd7` (post-MuxSurface-A1 main, 2026-08-27) → `945720b1ad25673d86e263cdd834532f` (post-Batch-10 branch tip `aa899a1e`, 2026-09-11, ~3.30 MB) → `25ffc88e9a13611f61871b4c4a5b17ab` (review-remediation F01–F20 wave, 2026-09-17, 3357.3 KB) → `3860c0264940e7df91511da493fa375e` (2026-09-26, 1677.8 KB) → **`35ca335a90da01ac631493f220aaf121`** (RR-021 wave, 2026-09-29, **1575.2 KB** — lazy esbuild/acorn/diff shims + ajv external + −767 dead lines) → **`20544cf9ea436c69b0b434462def91b9`** (2026-10-07, **1620.1 KB** / 1,658,985 B — pi 1.0.4 adoption + RPC transport wiring + U2-lite MCP cut). **Always check current**: `md5sum dist/index.mjs` |
364
+ | Bundle md5 anchors | `1cc4d55e18add7b9a036c569143320b6` (Phase-4 flip, ~2.78 MB) → `16e29d053bd370e24f40df147dadcb79` (v0.9.66, 2026-08-11) → `9b557ac106b82e1ee33d39dd0d6c7dd7` (post-MuxSurface-A1 main, 2026-08-27) → `945720b1ad25673d86e263cdd834532f` (post-Batch-10 branch tip `aa899a1e`, 2026-09-11, ~3.30 MB) → `25ffc88e9a13611f61871b4c4a5b17ab` (review-remediation F01–F20 wave, 2026-09-17, 3357.3 KB) → `3860c0264940e7df91511da493fa375e` (2026-09-26, 1677.8 KB) → **`35ca335a90da01ac631493f220aaf121`** (RR-021 wave, 2026-09-29, **1575.2 KB** — lazy esbuild/acorn/diff shims + ajv external + −767 dead lines) → **`20544cf9ea436c69b0b434462def91b9`** (2026-10-07, **1620.1 KB** / 1,658,985 B — pi 1.0.4 adoption + RPC transport wiring + U2-lite MCP cut) → **`b7194c0cbc5eee88e9add4b72685d951`** (2026-10-11, **1698.7 KB** / 1,738,680 B — Wave A 0.11.17: pi-1.1.0 adoption + env registry + OSC 7501). **Always check current**: `md5sum dist/index.mjs` |
349
365
 
350
366
  ---
351
367
 
@@ -312,6 +312,28 @@ function parseSurfacePolicy(value: unknown): CrewRuntimeConfig["surface"] | unde
312
312
  };
313
313
  }
314
314
 
315
+ /** U11 (hybrid 2-tier worker model): embedded-tier opt-in policy. Same
316
+ * field-wise-drop discipline as parseIsolationPolicy — invalid fields are
317
+ * dropped silently (never thrown) and an all-invalid block collapses to
318
+ * undefined so the tier stays DEFAULT-OFF for malformed input. */
319
+ function parseEmbeddedWorkersPolicy(value: unknown): CrewRuntimeConfig["embeddedWorkers"] | undefined {
320
+ const obj = asRecord(value);
321
+ if (!obj) return undefined;
322
+ const enabled = parseWithSchema(Type.Boolean(), obj.enabled);
323
+ const roles = parseStringList(obj.roles);
324
+ const maxConcurrent = parsePositiveInteger(obj.maxConcurrent, 1024);
325
+ const faultThreshold = parsePositiveInteger(obj.faultThreshold, 100);
326
+ if (enabled === undefined && roles === undefined && maxConcurrent === undefined && faultThreshold === undefined) {
327
+ return undefined;
328
+ }
329
+ return {
330
+ ...(enabled !== undefined ? { enabled } : {}),
331
+ ...(roles !== undefined ? { roles } : {}),
332
+ ...(maxConcurrent !== undefined ? { maxConcurrent } : {}),
333
+ ...(faultThreshold !== undefined ? { faultThreshold } : {}),
334
+ };
335
+ }
336
+
315
337
  /**
316
338
  * F19-1 (Round 19 parity): runtime.modelFallback was declared in types.ts and the
317
339
  * schema (PiTeamsModelFallbackConfigSchema) but parseRuntimeConfig never emitted
@@ -394,6 +416,7 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
394
416
  handoffBudgetTokens: parseHandoffBudgetTokens(obj.handoffBudgetTokens),
395
417
  agentExtensions: parseStringList(obj.agentExtensions),
396
418
  isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
419
+ embeddedWorkers: parseEmbeddedWorkersPolicy(obj.embeddedWorkers),
397
420
  surface: parseSurfacePolicy(obj.surface),
398
421
  modelFallback: parseModelFallbackConfig(obj.modelFallback),
399
422
  // F19-1 discipline: every key declared in types.ts + schema MUST be
@@ -401,7 +424,9 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
401
424
  // W2/P1-1; runtime.workerTransport — W7/P2-3, experimental;
402
425
  // runtime.hermeticWorkers — R3-19/D5 reversal, hermetic spawns;
403
426
  // runtime.classifierEnabled/classifierModel — P2-1 dormant classifier
404
- // seam, consumed at child-executor's retry triage).
427
+ // seam, consumed at child-executor's retry triage;
428
+ // runtime.embeddedWorkers — U11 hybrid 2-tier, DEFAULT-OFF, consumed by
429
+ // task-runner's lazy hybrid-tier planning).
405
430
  // Necessary but NOT sufficient: emission still needs a CONSUMER at the
406
431
  // seam — workerTransport was parse+emit-only dead until D1/DR2 threaded
407
432
  // it to resolveWorkerTransport (run-worker seam).
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { atomicWriteFile } from "../state/atomic-write.ts";
5
5
  import { withFileLockSync } from "../state/coordination/locks.ts";
6
6
  import { logInternalError } from "../utils/internal-error.ts";
7
- import { projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
7
+ import { isJunkPathLiteral, projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
8
8
  import { mergeConfig } from "./config-merge.ts";
9
9
  import { parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
10
10
  import { DEFAULT_BROKER, DEFAULT_NESTING, DEFAULT_PERSISTENCE, resolveBrokerEnvOverride } from "./defaults.ts";
@@ -164,6 +164,11 @@ export function invalidateConfigCache(): void {
164
164
  function resolveHomeDir(): string {
165
165
  const envValue = getCrewEnv("PI_CREW_HOME")?.trim();
166
166
  const defaultHome = os.homedir();
167
+ // Junk-literal guard (round3 audit, shared F4 helper): PI_CREW_HOME=$UNSET_VAR
168
+ // expands to the literal "undefined"/"null" — previously fell through to the
169
+ // containment check and could anchor config paths on a junk
170
+ // "undefined/.pi/…" tree. Treat as unset, fall back to the real home.
171
+ if (isJunkPathLiteral(envValue)) return defaultHome;
167
172
  if (!envValue) return defaultHome;
168
173
  // FIX (Round 14): When PI_TEAMS_HOME is explicitly set, validate that
169
174
  // it points within the real user home directory. This prevents a