pi-crew 0.11.6 → 0.11.8

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 (58) hide show
  1. package/.githooks/pre-commit +22 -0
  2. package/.githooks/pre-push +56 -0
  3. package/CHANGELOG.md +216 -1
  4. package/dist/index.mjs +531 -508
  5. package/docs/publishing.md +29 -0
  6. package/index.ts +3 -3
  7. package/package.json +9 -6
  8. package/scripts/install-hooks.mjs +32 -0
  9. package/skills/real-test-pi-crew/SKILL.md +31 -18
  10. package/src/config/config-validation.ts +35 -0
  11. package/src/config/env-vars.ts +38 -0
  12. package/src/config/types.ts +71 -0
  13. package/src/extension/registration/foreground-run-controller.ts +19 -10
  14. package/src/extension/registration/lifecycle-handlers.ts +6 -491
  15. package/src/extension/registration/lifecycle.ts +7 -90
  16. package/src/extension/registration/render-loop.ts +502 -0
  17. package/src/extension/team-tool/doctor.ts +6 -0
  18. package/src/prompt/prompt-runtime.ts +33 -4
  19. package/src/runtime/child-pi/child-pi-spawn.ts +14 -0
  20. package/src/runtime/child-pi/child-pi.ts +130 -6
  21. package/src/runtime/child-pi/pi-version.ts +187 -0
  22. package/src/runtime/child-pi/session-recovery.ts +334 -0
  23. package/src/runtime/classifier/classifier-service.ts +298 -0
  24. package/src/runtime/dispatch-batch.ts +4 -0
  25. package/src/runtime/live-session/live-session-runtime.ts +31 -0
  26. package/src/runtime/model/model-scope.ts +141 -21
  27. package/src/runtime/model/pi-args.ts +166 -6
  28. package/src/runtime/peer-dep.ts +24 -0
  29. package/src/runtime/process/zombie-scanner.ts +168 -7
  30. package/src/runtime/rpc/README.md +139 -0
  31. package/src/runtime/rpc/frame-client.ts +328 -0
  32. package/src/runtime/rpc/rpc-worker.ts +271 -0
  33. package/src/runtime/rpc/ui-request-policy.ts +112 -0
  34. package/src/runtime/run-worker.ts +91 -3
  35. package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
  36. package/src/runtime/skill-instructions.ts +66 -1
  37. package/src/runtime/surface/resolve-surface.ts +34 -19
  38. package/src/runtime/surface/surface-spawn.ts +8 -1
  39. package/src/runtime/task-output-context.ts +191 -32
  40. package/src/runtime/task-packet.ts +3 -0
  41. package/src/runtime/task-runner/child-executor.ts +172 -2
  42. package/src/runtime/task-runner/post-execution.ts +5 -3
  43. package/src/runtime/task-runner/pre-execution.ts +32 -1
  44. package/src/runtime/task-runner/prompt-builder.ts +10 -2
  45. package/src/runtime/task-runner.ts +6 -0
  46. package/src/schema/config-schema.ts +37 -0
  47. package/src/schema/team-tool-schema.ts +14 -2
  48. package/src/state/contracts.ts +5 -0
  49. package/src/ui/format-helpers.ts +11 -0
  50. package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
  51. package/src/ui/pi-ui-compat.ts +18 -9
  52. package/src/ui/rail.ts +14 -1
  53. package/src/ui/settings-overlay.ts +11 -2
  54. package/src/ui/widget/index.ts +0 -1
  55. package/src/ui/widget/task-list.ts +10 -3
  56. package/src/ui/widget/widget-renderer.ts +53 -4
  57. package/src/utils/relative-time.ts +11 -3
  58. package/src/utils/visual.ts +32 -2
@@ -76,6 +76,35 @@ Users can install the published package with:
76
76
  pi install npm:pi-crew
77
77
  ```
78
78
 
79
+ ### Pre-push hook (local release gate, D3)
80
+
81
+ The repo ships committed git hooks under `.githooks/`. A plain
82
+ `npm install` activates them via the `prepare` script
83
+ (`scripts/install-hooks.mjs` sets `git config core.hooksPath .githooks` —
84
+ idempotent, and a no-op when git or a repo is absent). Manual activation
85
+ in an existing clone:
86
+
87
+ ```bash
88
+ git config core.hooksPath .githooks
89
+ ```
90
+
91
+ Scope of `pre-push` (the release-gate broadening; `pre-commit` handles
92
+ lockfile drift):
93
+
94
+ - **push to `refs/heads/main`** — `check:conflict-markers` + `typecheck`,
95
+ then the **FULL unit suite** (`npm run test:unit`, the same gate CI
96
+ runs). Any failure blocks the push. This closes the 2026-10 gap where
97
+ local pushes ran only `test:critical` (~120 tests) while CI ran the full
98
+ suite — three waves shipped red that way.
99
+ - **push to any other ref** — fast gates only (`check:conflict-markers` +
100
+ `typecheck`).
101
+
102
+ Emergency bypass (CI still gates `main`):
103
+
104
+ ```bash
105
+ git push --no-verify
106
+ ```
107
+
79
108
  ### Postinstall
80
109
 
81
110
  `npm install` / `pi install` triggers `scripts/postinstall.mjs`, which:
package/index.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Resolution order:
5
5
  * 1. dist/index.mjs (bundle) when present AND not explicitly disabled.
6
- * Default since v0.9.17 close-out — benchmarks show bundle is
7
- * ~19% faster cold-start on a cold Node cache; with a warm
8
- * NODE_COMPILE_CACHE the gap shrinks to ~2%. The bundle's primary
6
+ * Default since v0.9.17 close-out — re-measured 2026-10-01 (deep review
7
+ * §10/V7): ~36% faster cold-start on a cold Node cache; the gap narrows
8
+ * with a warm NODE_COMPILE_CACHE. The bundle's primary
9
9
  * value is correctness (vendored typebox) and deterministic startup,
10
10
  * not raw speed.
11
11
  * See `scripts/bench-cold-start.mjs` for reproducible numbers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  "access": "public"
9
9
  },
10
10
  "engines": {
11
- "node": ">=22.0.0"
11
+ "node": ">=22.19.0"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
@@ -45,6 +45,8 @@
45
45
  "assets/crew-vibes.ttf",
46
46
  "scripts/clean-strip-types.mjs",
47
47
  "scripts/postinstall.mjs",
48
+ "scripts/install-hooks.mjs",
49
+ ".githooks/",
48
50
  "scripts/install-crew-vibes-font.mjs",
49
51
  "scripts/build-crew-vibes-font.py",
50
52
  "scripts/pty_probe.py",
@@ -101,6 +103,7 @@
101
103
  "build:bundle:once": "node scripts/watch-bundle.mjs --once",
102
104
  "dev": "node scripts/dev-runner.mjs",
103
105
  "postinstall": "node scripts/postinstall.mjs",
106
+ "prepare": "node scripts/install-hooks.mjs",
104
107
  "install:crew-font": "node scripts/install-crew-vibes-font.mjs",
105
108
  "build:crew-font": "python scripts/build-crew-vibes-font.py",
106
109
  "bench": "node scripts/run-bench.mjs",
@@ -147,10 +150,10 @@
147
150
  },
148
151
  "devDependencies": {
149
152
  "@biomejs/biome": "^2.5.14",
150
- "@earendil-works/pi-agent-core": "^1.0.0",
151
- "@earendil-works/pi-ai": "^1.0.0",
152
- "@earendil-works/pi-coding-agent": "^1.0.0",
153
- "@earendil-works/pi-tui": "^1.0.0",
153
+ "@earendil-works/pi-agent-core": "^1.0.4",
154
+ "@earendil-works/pi-ai": "^1.0.4",
155
+ "@earendil-works/pi-coding-agent": "^1.0.4",
156
+ "@earendil-works/pi-tui": "^1.0.4",
154
157
  "@sinclair/typebox": "^0.34.52",
155
158
  "@types/node": "^25.9.8",
156
159
  "tsx": "^4.23.15",
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // install-hooks.mjs — D3: point git at the committed .githooks directory.
3
+ //
4
+ // Idempotent + environment-tolerant: exits 0 when git is absent, when the
5
+ // cwd is not a git repo, or when `git config` fails for any reason — a
6
+ // developer convenience must never break `npm install` (the same reason
7
+ // postinstall.mjs is best-effort). Wired as the package.json `prepare`
8
+ // script so a plain `npm install` activates BOTH committed hooks:
9
+ // .githooks/pre-commit — lockfile-drift gate (check-lockfile-sync)
10
+ // .githooks/pre-push — release gate (conflict markers + typecheck,
11
+ // + FULL test:unit when pushing to refs/heads/main)
12
+ import { spawnSync } from "node:child_process";
13
+
14
+ function git(args) {
15
+ return spawnSync("git", args, { encoding: "utf8" });
16
+ }
17
+
18
+ function main() {
19
+ const probe = git(["rev-parse", "--is-inside-work-tree"]);
20
+ if (probe.error || probe.status !== 0) {
21
+ console.log("[install-hooks] no usable git here — skipping core.hooksPath setup");
22
+ return;
23
+ }
24
+ const set = git(["config", "core.hooksPath", ".githooks"]);
25
+ if (set.error || set.status !== 0) {
26
+ console.warn("[install-hooks] could not set core.hooksPath (non-fatal):", set.stderr?.trim() ?? set.error?.message);
27
+ return;
28
+ }
29
+ console.log("[install-hooks] git core.hooksPath → .githooks (pre-commit + pre-push active)");
30
+ }
31
+
32
+ main();
@@ -3,7 +3,7 @@ name: real-test-pi-crew
3
3
  description: >
4
4
  LIVE end-to-end battery for pi-crew changes — unit-green ≠ works in a real Pi session; this skill proves the latter.
5
5
  USE WHEN: you changed src/runtime/broker|child-pi|surface|prompt|state, src/ui, src/config, src/schema/team-tool-schema.ts, agents/*.md, workflows, CI, or dist/ — before committing; a worker hung/died/"no output for 600000ms"; a run went green but panes/UI/ask/steer look wrong; someone says "verify it really works".
6
- TIERS AT A GLANCE: T1 critical (21s) · T2 kill-switch · T3 bundle+md5 · T4/T8 live-session sync · T5/T6 TUI probes · T7 smoke run · T9 team-tool feature battery · T10 surface panes (tmux + herdr) · T11 regression battery (a–j) · T12 agent-frontmatter contracts · T13 real-run UI render. A decision table at the TOP of the file maps changed-path → required tiers.
6
+ TIERS AT A GLANCE: T1 critical (~19s) · T2 kill-switch · T3 bundle+md5 · T4/T8 live-session sync · T5/T6 TUI probes · T7 smoke run · T9 team-tool feature battery · T10 surface panes (tmux + herdr) · T11 regression battery (a–j) · T12 agent-frontmatter contracts · T13 real-run UI render. A decision table at the TOP of the file maps changed-path → required tiers.
7
7
  When NOT to use: unit tests for isolated modules (use the test runner directly); pure test execution.
8
8
 
9
9
  origin: pi-crew
@@ -86,7 +86,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
86
86
 
87
87
  | Tier | Chứng minh gì | Chi phí | Section |
88
88
  |---|---|---|---|
89
- | 1 | critical suites xanh (broker/UI) | 21s | Tier 1 |
89
+ | 1 | critical suites xanh (broker/UI) | ~19s | Tier 1 |
90
90
  | 2 | kill-switch tắt đúng qua 3 đường | 75s | Tier 2 |
91
91
  | 3 | typecheck + rebuild bundle + md5 sync | 25s | Tier 3 |
92
92
  | 4 | session sống nhận được bundle mới | <1s + restart | Tier 4 |
@@ -123,6 +123,11 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
123
123
  | `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 |
124
124
  | `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 |
125
125
  | `dist/` (bundle) | T3 → T4 → T8 → T11j |
126
+ | `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 |
127
+ | `src/runtime/classifier/**` + config ceremony (`classifierEnabled` / `classifierModel`) | `classifier-service.test.ts` (14) + `child-executor-retry-triage.test.ts` (6) + `npm run check:env-vars` — unit-only, KHÔNG live tier (seam DORMANT default off; chỉ live khi `PI_CREW_CLASSIFIER_ENABLED=1`) |
128
+ | `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) |
129
+ | `.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.`) |
130
+ | `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 |
126
131
  | bất kỳ đường dẫn trên, trước khi commit | gates đầy đủ: critical + unit + tsc + biome + md5 |
127
132
 
128
133
  **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.
@@ -140,6 +145,7 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
140
145
  > **2026-09-29 update (RR-021 optimization wave + 4 review rounds, baseline `79a758d2`..`4393858c`, 37 commits; story `docs/stories/RR-021.md`; audit `pi-crew-optimization-audit.md`)**: (1) **Correctness/hang-risks** — `classifyReviewOutcome` word-boundary fix (the `\baccept\b` string-literal bug + negation guard), git/rg probe timeouts, `withRunLockSync`→`withRunLock` rerouting (lock family v0.9.26 intact), `reconcileAllStaleRuns` concurrent (`mapConcurrent` bound 4, per-run error isolation), sleeps→waitFor. (2) **Bundle hygiene** — esbuild/acorn/diff load via lazy sync-createRequire shims, ajv external, dead code −767 lines: bundle 3360 KB → **1575.2 KB**; NEW gates `check:lazy-imports` (ci.yml:142; src-side `// LAZY:` marker enforcement + dist-side no-hoisted-heavy-import assertion) and `check:bundle-size` (ci.yml:169). (3) **Test-gate integrity (review rounds 2–4)** — the runner now fails LOUDLY on `# cancelled N>0` (dual reporter: stdout UX unchanged, full TAP to a temp file, scraped in BOTH single-run and batch paths — `parseCancelledCount` + 21-test `test-runner-exit.test.ts`), on explicitly-listed files that do not exist (node:test v22 silently skipped them → exit 0; round-3 MAJOR B1), and `test:bundle` routes through the guarded runner (was the only CI gate bypassing it). Incremental tsc: typecheck ~20s → **~3.5s warm**. Suite anchors @ 2026-09-29: unit **8204 tests / 8201 pass / 0 fail / 3 skipped** (909 files), integration **136 / 132 pass / 4 skipped** (35 files incl. 3 slow), `test:critical` **116** (~18s), bundle **1575.2 KB** md5 `35ca335a90da01ac631493f220aaf121` (`--committed-hash` OK). Line refs moved: `PI_CREW_TEST_RUNNER_TIMEOUT_MS` now `scripts/test-runner.mjs:419`; crash-recovery design comment `:228`.
141
146
 
142
147
  > **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`.
148
+ > **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**.
143
149
  > **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.
144
150
  >
145
151
  > **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).
@@ -202,11 +208,11 @@ The skill maps to existing CI gates as follows:
202
208
  | Bundle-staleness check (incl. **ARCH-7 path-leak scan** since `7d18508b` — line-scans `dist/index.mjs` + structural sourcemap check for tracked-source leaks) | Tier 3 last step | `scripts/check-bundle-staleness.mjs`; `--committed-hash` mode = Tier 11j release gate |
203
209
  | `npm run test:bundle` (bundle import smoke, 2 tests) | Tier 3 post-build sanity | `test/unit/bundle-load.test.ts` — routed through the guarded `scripts/test-runner.mjs` since RR-021 round-3 (was a bare `node --test` bypass, the only CI gate without the wrappers) |
204
210
  | `node scripts/release-smoke.mjs` (manual, release cut) | Tier 3/11j companion | ARCH-6: installs pi-* peer deps, `import()`s the tarball-installed bundle (`:77`), shape-checks exports |
205
- | Full `npm test` (= unit 909 files + integration 35 incl. 3 slow @ 2026-09-29) | n/a — too slow for in-loop | CI only; slow tier (3 files) is a SEPARATE glob `test:integration:slow` — only `npm run test:full` includes it |
211
+ | Full `npm test` (= unit 968 files + integration 32 + 3 slow @ 2026-10-07) | n/a — too slow for in-loop | CI only; slow tier (3 files) is a SEPARATE glob `test:integration:slow` — only `npm run test:full` includes it |
206
212
  | `PI_CREW_SMOKE=1` env | Tier 11e | set ONLY in `weekly-smoke.yml` (auth-gated); nightly.yml deliberately does NOT (comment at `:24`) |
207
213
  | `npm run check:decision-drift` | Tier 11 (doc↔code) | **build-blocking in `.github/workflows/ci.yml` since 2026-09-17** (review-remediation baseline): every `PI_CREW_*` token cited in `docs/decisions/*.md` must exist in `src/` |
208
214
  | `npm run check:env-vars` | Tier 11 | **build-blocking in `ci.yml` since 2026-09-17**: every `PI_CREW_*` read must be routed via `getCrewEnv` + registered |
209
- | `npm run check:event-types -- --enforce` | Tier 9a/11 | **build-blocking in `ci.yml` since 2026-09-17**: event registry (`src/state/contracts.ts`) must cover every emitted type — 168 registered / 0 drift @ 2026-09-17 (was 89, drift silent); **169 @ 2026-10-01** sau SDD-2 (+`task.mcp_enforcement_degraded`) |
215
+ | `npm run check:event-types -- --enforce` | Tier 9a/11 | **build-blocking in `ci.yml` since 2026-09-17**: event registry (`src/state/contracts.ts`) must cover every emitted type — 168 registered / 0 drift @ 2026-09-17 (was 89, drift silent); **169 @ 2026-10-01** sau SDD-2 (+`task.mcp_enforcement_degraded`); **176 registered / 141 emitted @ 2026-10-07** — đếm bằng chính enforcement script (trong `TEAM_EVENT_TYPES` có 1 token trùng: 177 tokens / 176 unique) |
210
216
  | `npm run check:lazy-imports` | Tier 3 companion (RR-021 WI-2.3 + review rounds 2-4) | `scripts/check-lazy-imports.mjs` — src-side: every runtime `import()` position needs a `// LAZY:` marker (known gaps: ternary-else / object-property / case-label / runtime member-access after same-line colon — documented in-script); dist-side: no module-scope hoisted heavy import (esbuild/acorn/diff/jiti/cli-highlight/yaml/ajv). Build-blocking at `ci.yml:142` |
211
217
  | `npm run check:bundle-size` | Tier 3 companion | `scripts/check-bundle-size.mjs` — committed bundle ≤ budget (Sprint 6 gate, `ci.yml:169`; budget re-anchored when RR-021 shrank the bundle 3360→1575 KB) |
212
218
  | `PI_CREW_TEST_RUNNER_TIMEOUT_MS` env | Tier 1 companion | overrides the test-runner spawn deadline (`scripts/test-runner.mjs:419` since RR-021; was :206); default **1_500_000 ms** since 2026-09-17 (was 900_000 — sized for a 5800-test suite; the full suite outgrew it and the ETIMEDOUT became a false green pre-F05). Since RR-021 rounds 2-3 the runner ALSO fail-closes on `# cancelled N>0` (TAP dual-reporter scrape) and on explicitly-listed missing files — see `test/unit/scripts/test-runner-exit.test.ts` (21 tests) |
@@ -235,11 +241,11 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
235
241
 
236
242
  ---
237
243
 
238
- ## Tier 1 — Critical unit tests (~21s, 116 tests, the only suite you need for broker/UI changes)
244
+ ## Tier 1 — Critical unit tests (~19s, 120 tests, the only suite you need for broker/UI changes)
239
245
 
240
- **What**: run the curated 14-file fast subset.
246
+ **What**: run the curated 15-file fast subset.
241
247
 
242
- **Why this exists**: full `npm run test:unit` runs 909 files (was 642 at skill-writing time — it keeps growing), several minutes. Verifier worker response timeout would kill the worker mid-run → run = "hang". The fix (introduced in commit `1cb2dca`) splits out a `test:critical` subset covering exactly what changed in the broker/UI work.
248
+ **Why this exists**: full `npm run test:unit` runs 968 files (was 642 at skill-writing time — it keeps growing), several minutes. Verifier worker response timeout would kill the worker mid-run → run = "hang". The fix (introduced in commit `1cb2dca`) splits out a `test:critical` subset covering exactly what changed in the broker/UI work.
243
249
 
244
250
  **How**:
245
251
 
@@ -247,16 +253,16 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
247
253
  time npm run test:critical
248
254
  ```
249
255
 
250
- Expected output: `# tests 116 # pass 116 # fail 0 # cancelled 0 # duration_ms ~18000`. (Count was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, **116 since 2026-09-20** — verify with the actual run; the skill's hard-coded numbers drift between releases.) **Since RR-021 round-2 the runner itself fails loudly on `# cancelled N>0`** (dual-reporter TAP scrape — the round-1 unref incident shipped green with 2 cancelled tests), and **round-3 added the missing-file pre-check**: an explicitly-listed file that does not exist now exits 1 naming the path instead of being silently skipped by node:test.
256
+ Expected output: `# tests 120 # pass 120 # fail 0 # cancelled 0 # duration_ms ~19965`. (Count was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 since 2026-09-20, **120 @ 2026-10-07** (+`child-executor-heartbeat-pulse.test.ts` → 15 files) — verify with the actual run; the skill's hard-coded numbers drift between releases.) **Since RR-021 round-2 the runner itself fails loudly on `# cancelled N>0`** (dual-reporter TAP scrape — the round-1 unref incident shipped green with 2 cancelled tests), and **round-3 added the missing-file pre-check**: an explicitly-listed file that does not exist now exits 1 naming the path instead of being silently skipped by node:test.
251
257
 
252
258
  **References**:
253
259
 
254
260
  | What | Where |
255
261
  |---|---|
256
- | Script definition | `package.json:85` — list of 14 files passed to `node scripts/test-runner.mjs` |
262
+ | Script definition | `package.json:91` — list of 15 files passed to `node scripts/test-runner.mjs` |
257
263
  | Introduced in commit | `1cb2dca fix(verifier): use test:critical instead of test:unit to avoid worker timeout` |
258
264
  | Runner wrapper | `scripts/test-runner.mjs` — injects `--test-force-exit`, forwards to `tsx --test` |
259
- | The 14 files | broker: `test/unit/runtime/broker/crew-broker-{handshake,stale-socket,feature-flag,server-gate,client-fallback,mailbox-observer,close-during-reconnect,steer-dedup,symlink-steering}.test.ts`; UI: `test/unit/ui/keybinding-map.parity.test.ts`, `test/unit/ui/pi-tui-dispatch-probe.test.ts`; utils: `test/unit/utils/session-utils-extract.test.ts`; config: `test/unit/config/config-schema-sync.test.ts`; spawn env: `test/unit/runtime/child-pi/child-pi-env-spread.test.ts` |
265
+ | The 15 files | broker: `test/unit/runtime/broker/crew-broker-{handshake,stale-socket,feature-flag,server-gate,client-fallback,mailbox-observer,close-during-reconnect,steer-dedup,symlink-steering}.test.ts`; UI: `test/unit/ui/keybinding-map.parity.test.ts`, `test/unit/ui/pi-tui-dispatch-probe.test.ts`; utils: `test/unit/utils/session-utils-extract.test.ts`; config: `test/unit/config/config-schema-sync.test.ts`; spawn env: `test/unit/runtime/child-pi/child-pi-env-spread.test.ts`; heartbeat: `test/unit/runtime/task-runner/child-executor-heartbeat-pulse.test.ts` (15th file @ 2026-10-07) |
260
266
  | Failure mode that motivates it | Worker timeout in `src/runtime/child-pi/child-pi-constants.ts:23` (`RESPONSE_TIMEOUT_MS = DEFAULT_CHILD_PI.responseTimeoutMs` — 300000 at the time, now 600000); verifier LLM ran `npm test` and got killed with exit 143 (SIGTERM) |
261
267
 
262
268
  **Run after**: any edit to `src/runtime/broker/*.ts`, `src/ui/`, `src/config/`, `src/extension/registration/lifecycle-handlers.ts`, or `src/runtime/child-pi/*.ts`.
@@ -287,7 +293,7 @@ PI_CREW_BROKER=0 npm run test:critical
287
293
  PI_CREW_BROKER=1 npm run test:critical
288
294
  ```
289
295
 
290
- All three must show `# pass 101 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
296
+ All three must show `# pass 120 # fail 0`. Measured times in this session (2026-08-11): ~26s for default, ~26s for `PI_CREW_BROKER=0`, ~26s for `PI_CREW_BROKER=1` (varies ±1-2s run-to-run).
291
297
 
292
298
  **References**:
293
299
 
@@ -328,7 +334,7 @@ Compare the printed md5 against what the user's Pi session loaded. If they diffe
328
334
  | Bundle builder | `scripts/build-bundle.mjs` (esbuild-based, bundles `index.bundle.ts` → `dist/index.mjs`) |
329
335
  | 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** |
330
336
  | Postinstall hook | `scripts/postinstall.mjs:43` — best-effort bundle rebuild; falls back to strip-types if esbuild missing |
331
- | 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). **Always check current**: `md5sum dist/index.mjs` |
337
+ | 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` |
332
338
 
333
339
  ---
334
340
 
@@ -474,7 +480,7 @@ else:
474
480
 
475
481
  **What**: prove the verifier worker completes within `RESPONSE_TIMEOUT_MS` (**600s since the stuck-worker hardening — was 300s when this skill was distilled; `DEFAULT_CHILD_PI.responseTimeoutMs = 10 * 60_000`**).
476
482
 
477
- **Why this is its own tier**: `test:critical` covers unit-level invariants, but the verifier LLM is a separate failure mode — it reads the verifier prompt from `src/runtime/goal-workflow/plan-templates.ts:144, 147` (taskTemplate strings) or from `workflows/*.workflow.md` (workflow verifier sections), then decides which bash command to run. If the prompt says "Run tests" without specifying which, the LLM runs `npm test` (875 files) and the worker gets killed by the response timeout with exit 143.
483
+ **Why this is its own tier**: `test:critical` covers unit-level invariants, but the verifier LLM is a separate failure mode — it reads the verifier prompt from `src/runtime/goal-workflow/plan-templates.ts:144, 147` (taskTemplate strings) or from `workflows/*.workflow.md` (workflow verifier sections), then decides which bash command to run. If the prompt says "Run tests" without specifying which, the LLM runs `npm test` (968 files) and the worker gets killed by the response timeout with exit 143.
478
484
 
479
485
  **How** (from parent Pi session — `team` is a tool, not a shell command):
480
486
 
@@ -788,7 +794,7 @@ grep -n "Log the event first" src/runtime/recovery/crash-recovery.ts # design
788
794
  # 3. buffered-site census — snapshot & audit:
789
795
  grep -rln "appendEventBuffered" src/ | wc -l # 15 files @ 2026-09-29 (16 at v0.10.5; RR-021 WI-4.2 dead-code sweep removed one); audited live conversions = 43; EVERY new site needs the reader-audit
790
796
  # 4. the full gate — test:critical has NO stores/dwf/recovery coverage:
791
- npm run test:unit # 875 files, ~8006 tests, ~11-12 min (670-730s measured 2026-09-20; 1088s under load 2026-09-21) — MANDATORY after any delayed-write conversion program
797
+ npm run test:unit # 968 files, ~8711 tests @ 2026-10-07, ~13 min (785s node measured 2026-10-07; 1088s under load 2026-09-21) — MANDATORY after any delayed-write conversion program
792
798
  ```
793
799
 
794
800
  ### 11b. wc-gate enforcement (M4 done-gate)
@@ -1088,7 +1094,7 @@ PY
1088
1094
  | Test using real `loadConfig()` to mock config | Flaky when env / disk config changes | `612e18b` | `test/unit/runtime/broker/crew-broker-server-gate.test.ts:78` (use `brokerEnv: "0"` instead of `flagOn: false`) |
1089
1095
  | Source edit seen immediately | No, requires bundle rebuild + reload | n/a (permanent) | `index.ts:1-25` — bundle resolution rules |
1090
1096
  | Skip disabled-path proof | `effectiveEnabled()` regression slips through | n/a (permanent) | Tier 2 above |
1091
- | `npm run test:unit` against the full suite (909 files now, 642 then) | several minutes; mis-judges verifier runtime | n/a (permanent) | Tier 1 above |
1097
+ | `npm run test:unit` against the full suite (968 files now, 642 then) | several minutes; mis-judges verifier runtime | n/a (permanent) | Tier 1 above |
1092
1098
  | Skip typecheck | TS errors slip past `test:critical` (which uses `--test-timeout=30000`) | n/a (permanent) | Tier 3 above |
1093
1099
  | Run `pi` from a stale bundle | Session shows old behavior despite src/ edits | n/a (permanent) | `scripts/check-bundle-staleness.mjs` — CI gate |
1094
1100
  | Test by reading code | Proves nothing about runtime | n/a (permanent) | All tiers above |
@@ -1124,6 +1130,11 @@ PY
1124
1130
  | **Declaring a red gate "pre-existing" (or "my regression") from reasoning alone** (2026-09-16): `check:env-vars` was red; the file was untouched, but that is an argument, not evidence. | n/a (process) | Prove it in a clean-HEAD worktree: `git worktree add -q /tmp/pc-head HEAD && (cd /tmp/pc-head && node scripts/check-env-vars.mjs); git worktree remove --force /tmp/pc-head` | Same recipe as the broker-flake proof (Tier 1 / Failure symptoms) |
1125
1131
  | **Shipping a catalog PNG with tofu boxes** (2026-09-16): the catalog rendered the braille spinner + `⟳` + `⎿` as `□` — invisible in the `.txt` captures, obvious the moment the image was opened. | `docs/ui-samples/render_png.py` glyph self-check | Make the renderer FAIL on any character it cannot paint (compare against the `.notdef` box), then map the offender | Tier 13 "Catalog" + `render_png.py` (`_has_glyph`, `BRAILLE_TO`) |
1126
1132
  | **Treating `wait-request-broker.test.ts` load-timeout as a product bug**: the test runner's per-file 180s timeout is below this file's full-suite runtime under parallel load — fails only with the whole suite, passes in isolation. Pre-existing flake, NOT a regression from your change. | n/a (test infra) | Re-run the single file before fixing anything: `node scripts/test-runner.mjs test/unit/runtime/broker/wait-request-broker.test.ts`. Green in isolation = infra flake; move on. |
1133
+ | **Pipe nuốt exit code của gate** (GH#62 wave, `51adb1e1`): `npm run typecheck \| tail -1` trả về exit của `tail` (0) — một type error từng được ship vì gate chạy qua pipe. | n/a (process) | KHÔNG bao giờ pipe một gate qua `tail`/`head`; chạy thẳng, hoặc `set -o pipefail` khi bắt buộc. Commit `51adb1e1` "pipe-swallowed gates in the previous commit" |
1134
+ | **Fake cache shape sai** (GH#62 CI-shard catch, `3ed0067e`): fake `new Map()` thiếu `.list()` → crash ở manifestCache path khi test chạm code thật. | `3ed0067e` | Dùng factory thật `createManifestCache(tmpdir)` (`src/runtime/manifest-cache.ts:218`, `.list()` :306) thay vì hand-rolled Map giả |
1135
+ | **URL win32 không đối xứng**: path Windows (`C:\…`) không phải URL hợp lệ khi truyền qua argv/env giữa process | n/a (permanent) | Luôn `pathToFileURL()` phía gửi + `fileURLToPath()` phía nhận, đối xứng với nguồn — tham chiếu cặp đang dùng: `src/runtime/peer-dep.ts:40,90,211` |
1136
+ | **RT-11 scan đếm cả comment prose** (`48cc088d`): static spawn-site scan regex `spawn\s*\(` match CẢ text trong comment — một comment "spawn (and…" thành call-site thứ 4 trong khi chỉ có 3 call thật + register | `48cc088d` | **Reword COMMENT, KHÔNG relax test** — enforcer là `test/unit/runtime/child-pi/child-pi-spawn-registration.test.ts:33` (regex cố ý lỏng) |
1137
+ | **Stale-test-after-refactor class — 3 vòng CI đỏ liền 2026-10-04/05**: test pin layout/text cũ không theo kịp refactor lành tính (literal-count 4→5 subsections sau khi chia section — `test/unit/runtime/core/task-handoff-template.test.ts`, commit `d7923d38`; OSC-8 dock-link regex — `test/unit/ui/widget-osc8-link.test.ts`, commit `db70cc82`; T-6b storm tick — `test/unit/runtime/task-runner/state-helpers-cas-contention.test.ts`, commit `57f1bbf8`). Hook header gọi đúng: "three waves in a row shipped red" | n/a (process) | Full suite trước push — giờ `.githooks/pre-push` enforce trên push main (update 2026-10-07); refactor layout/text → cập nhật pin test CÙNG commit |
1127
1138
 
1128
1139
  ---
1129
1140
 
@@ -1158,6 +1169,8 @@ When a tier fails, the recovery is usually quick. Match the symptom to the cause
1158
1169
  | Ambient "N dead worker(s)" notification for a run that already reached a terminal status | Stale health marker re-firing on a terminal run — no live process behind it | Verify before acting: `ps -p <pid>` (dead), `manifest.status` (failed/done), `exit-code.txt`. Nothing to recover; marker clears via `/team-dashboard → health → K` (kill stale). Never kill a pid on a stale heartbeat alone — check pid + log first |
1159
1170
  | Ambient "heartbeat dead"/"missing heartbeat" cho task guest `gc-*` (depth-2 delegate) trên run ĐANG CHẠY | Guest tasks không có kênh heartbeat (lifecycle do `delegate.requested/admitted/completed` sở hữu) — fixed 2026-09-26: watcher `73790eaa` + aggregator `b7ab5ef2` skip `agent==="delegate"` ở CẢ HAI lớp | Nếu tái xuất hiện trên build ≥ `b7ab5ef2`: check skip marker còn nguyên ở cả 2 nơi (watcher task-loop + `summarizeHeartbeats`). KHÔNG bao giờ kết luận worker thật chết chỉ từ notification của guest task — verify pid/manifest như row trên |
1160
1171
  | Battery async run "xanh" nhưng event `async.spawned` có `data.inline:true`, hoặc không có process runner detached nào trong `ps` | **Seam leak**: shell có `PI_CREW_TEST_ASYNC_INLINE=1` (thường + `PI_CREW_ALLOW_MOCK=1`) export từ phiên test trước — async dispatch chạy in-process, battery đang chứng minh đường SAI (seam là unit-test tool) | Preflight battery: `env | grep PI_CREW_TEST_ASYNC_INLINE` phải RỖNG; sau mỗi async run trong T7/T9b: assert `async.spawned` KHÔNG có `data.inline`. Nếu dính: `unset` cả 2 biến rồi chạy lại tier |
1172
+ | Integration đỏ macOS/Node-24 `phase6` "argv must not contain task text" với task NGẮN (empty task vẫn xanh) — code giống hệt run xanh trước | **Random-path collision flake** (CI 2026-10-07): `@<mkdtemp-path>` argv arg chứa suffix ngẫu nhiên 6 ký tự [a-zA-Z0-9]; task 3 ký tự có P ≈ 1/15k bị substring-collision với suffix → assert `arg.includes(task)` tưởng là leak | Scan argv LOẠI arg `@<taskPath>` chính nó (fix `f67cf62d`) — path là random mkdtemp, không phải nội dung task; never "fix" bằng đổi task text dài hơn để trốn collision |
1173
+ | `phase6-runtime-hardening.test.ts` "depth guard blocks child workers at max depth before mock execution" fails 2/2 khi chạy từ worker shell dù xanh trong CI | **Env leak, không phải regression**: worker shell export sẵn `PI_CREW_MAX_DEPTH=4`/`PI_TEAMS_MAX_DEPTH=4` và `resolveCrewMaxDepth` ưu tiên ENV hơn `inputMaxDepth` tường minh (`src/runtime/model/pi-args.ts:128` — `env.PI_CREW_MAX_DEPTH ?? env.PI_TEAMS_MAX_DEPTH` check TRƯỚC input) → test set `maxDepth:2` vẫn thấy 4, `blocked:false` | Scrub ĐỦ biến (danh sách `-u` tĩnh KHÔNG đủ — biến thể của gotcha 2026-08-15): `SCRUB=$(env \| grep -oE '^(PI_CREW\|PI_TEAMS)_[A-Z_]+' \| sed 's/^/-u /' \| tr '\n' ' '); env $SCRUB npm run test:integration` |
1161
1174
  | Steer written during worker BOOT never reaches the model (no error, no log; later steers work) | F-L2 (fixed 2026-09-21): poll advanced its offset past an entry whose `pi.sendMessage` threw pre-bind ("not initialized") and the per-line catch ate the throw — one-shot silent loss | Fixed in `prompt-runtime.ts` (offset rewind + dedup unmark); pinned by `prompt-runtime-steer-boot-window.test.ts`. On older builds: re-send the steer after ~5s — a steer that lands post-bind delivers fine |
1162
1175
  | Steer "lost" but the file you wrote is FULL while a 0-byte `<taskId>.jsonl` sits beside it | **Probe bug, not a product bug** — the steering filename depends on run kind: team runs poll `<taskId>.jsonl`, crew_agent/subagent runs use `<slot>-agent.jsonl`; you wrote the wrong channel (no component reads it) | Re-run writing every `*.jsonl` in the steering dir (Tier 9g), or derive the name from the run kind. Cross-check `results/<taskId>.txt` + the worker session's `custom_message` before calling it a loss |
1163
1176
  | Fix verified in unit tests but a live probe still fails | Live path ≠ unit path: wrong filename (above), wrong role/tool for the probe instruction, or the session did not cold-start the changed module | Confirm the loaded artifact (`--extension <path>` resolution for workers is the SOURCE file, so a restart is enough; parent-side needs bundle+restart), then re-run Tier 9g with text-only compliance |
@@ -1352,7 +1365,7 @@ The skill mentions specific commits, line numbers, and version pins. As the code
1352
1365
  | Verify line refs after each `src/` commit | Every commit touching the cited file | `git log -p -- src/extension/registration/lifecycle-handlers.ts \| grep effectiveEnabled` — if line moved, update the skill |
1353
1366
  | Verify commit hashes still exist | Quarterly or before major edits | `git log --oneline -1 <hash>` — if gone, find the equivalent newer commit |
1354
1367
  | Verify version pins (v0.9.46, etc.) | Each release | `git log --oneline -- src/ui/run-dashboard.ts \| head -5` — confirm diag removal history (e3ee6fe2) still accurate |
1355
- | Verify `test:critical` still has 14 files | Each `src/runtime/broker/*.ts` edit | `grep test:critical package.json` — adjust the file list |
1368
+ | Verify `test:critical` still has 15 files | Each `src/runtime/broker/*.ts` edit | `grep test:critical package.json` — adjust the file list |
1356
1369
  | Verify Tier 7 verifier prompts still say `test:critical` | Each workflow file edit | `grep "Run FAST checks" workflows/*.workflow.md` |
1357
1370
  | Verify Tier 10 surface refs | Each `src/runtime/surface/**` edit | `ls test/system/surface-*.e2e.test.ts` + grep `MAX_SURFACE_WORKERS` in resolve-surface.ts — cap/config shape may drift between A1 → A2 |
1358
1371
  | Verify herdr wire details | Each herdr release bump | `herdr api schema --json` vs `src/runtime/surface/herdr-provider.ts` (envelope/pane.read source/1-conn-per-request were verified on herdr 0.8.2) |
@@ -1373,7 +1386,7 @@ The skill does NOT need to be updated for every commit — only when the cited l
1373
1386
  ## Quick reference — exact commands
1374
1387
 
1375
1388
  ```bash
1376
- # Tier 1 (critical unit, ~21s, 116 tests)
1389
+ # Tier 1 (critical unit, ~19s, 120 tests)
1377
1390
  npm run test:critical
1378
1391
  # Tier 2 (3-path proof, broker changes only)
1379
1392
  PI_CREW_BROKER=0 npm run test:critical
@@ -1457,7 +1470,7 @@ PY
1457
1470
 
1458
1471
  Before claiming "tested":
1459
1472
 
1460
- - [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, **116 @ 2026-09-20**; record the actual count in the report.
1473
+ - [ ] Tier 1: `test:critical` fresh-run, all pass (<25s). Count varies by release — was 97 at v0.9.46, 101 at v0.9.66, 102 at the waitMethodsEnabled flip, 116 @ 2026-09-20, **120 @ 2026-10-07**; record the actual count in the report.
1461
1474
  - [ ] Tier 2: 3-path proof all pass — **required if you touched `src/config/defaults.ts` or `src/extension/registration/lifecycle-handlers.ts`**
1462
1475
  - [ ] Tier 3: `npm run typecheck` exit 0, `npm run build:bundle` exit 0
1463
1476
  - [ ] Tier 4: bundle md5 matches what the session loaded (or user has `/quit`-ed + reopened)
@@ -77,6 +77,11 @@ const LIMIT_CEILINGS = {
77
77
  // (10_000 turns), which capped the effective timeout at 10s and silently disabled
78
78
  // any larger value (e.g. 300_000 = 5min) via parsePositiveInteger returning undefined.
79
79
  runtimeTaskTimeoutMs: 24 * 60 * 60 * 1000,
80
+ // Handoff budget (est tokens, chars/4) for the dynamic.dependencyContext
81
+ // prompt layer. Above the ceiling the budget is treated as the OFF-switch
82
+ // (untrimmed render), mirroring the render-site semantics in
83
+ // task-output-context.ts.
84
+ runtimeHandoffBudgetTokens: 1_000_000,
80
85
  nestingMaxSlots: 64,
81
86
  nestingMaxDepth: 10,
82
87
  } as const;
@@ -325,6 +330,21 @@ function parseModelFallbackConfig(value: unknown): CrewRuntimeConfig["modelFallb
325
330
  return Object.values(modelFallback).some((entry) => entry !== undefined) ? modelFallback : undefined;
326
331
  }
327
332
 
333
+ /**
334
+ * Handoff budget for the dynamic.dependencyContext prompt layer (est tokens,
335
+ * chars/4). Deliberately NOT parsePositiveInteger: out-of-range values are
336
+ * dropped silently (undefined → render site falls back to default 1800)
337
+ * rather than rejecting the whole runtime block. The ≤0 / >1_000_000
338
+ * OFF-switch is expressible only via env PI_CREW_HANDOFF_BUDGET_TOKENS
339
+ * (env bypasses this parse and resolves first); the config schema documents
340
+ * 1..1_000_000 as the valid range.
341
+ */
342
+ function parseHandoffBudgetTokens(value: unknown): number | undefined {
343
+ if (typeof value !== "number" || !Number.isInteger(value)) return undefined;
344
+ if (value < 1 || value > LIMIT_CEILINGS.runtimeHandoffBudgetTokens) return undefined;
345
+ return value;
346
+ }
347
+
328
348
  function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
329
349
  const obj = asRecord(value);
330
350
  if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
@@ -362,10 +382,25 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
362
382
  return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
363
383
  })(),
364
384
  excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
385
+ handoffBudgetTokens: parseHandoffBudgetTokens(obj.handoffBudgetTokens),
365
386
  agentExtensions: parseStringList(obj.agentExtensions),
366
387
  isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
367
388
  surface: parseSurfacePolicy(obj.surface),
368
389
  modelFallback: parseModelFallbackConfig(obj.modelFallback),
390
+ // F19-1 discipline: every key declared in types.ts + schema MUST be
391
+ // emitted here or the config key is dead (runtime.sessionRecovery —
392
+ // W2/P1-1; runtime.workerTransport — W7/P2-3, experimental;
393
+ // runtime.hermeticWorkers — R3-19/D5 reversal, hermetic spawns;
394
+ // runtime.classifierEnabled/classifierModel — P2-1 dormant classifier
395
+ // seam, consumed at child-executor's retry triage).
396
+ // Necessary but NOT sufficient: emission still needs a CONSUMER at the
397
+ // seam — workerTransport was parse+emit-only dead until D1/DR2 threaded
398
+ // it to resolveWorkerTransport (run-worker seam).
399
+ sessionRecovery: parseWithSchema(Type.Boolean(), obj.sessionRecovery),
400
+ hermeticWorkers: parseWithSchema(Type.Boolean(), obj.hermeticWorkers),
401
+ workerTransport: parseWithSchema(Type.Union([Type.Literal("stdio"), Type.Literal("rpc")]), obj.workerTransport),
402
+ classifierEnabled: parseWithSchema(Type.Boolean(), obj.classifierEnabled),
403
+ classifierModel: parseWithSchema(Type.String({ minLength: 1 }), obj.classifierModel),
369
404
  };
370
405
  return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
371
406
  }
@@ -317,6 +317,11 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
317
317
  name: "PI_CREW_DISABLE_RESULT_READ_CACHE",
318
318
  doc: "'1' bypasses the per-run result-artifact read cache (R10-1 bypass control; task-output-context.ts createResultArtifactReadCache)",
319
319
  },
320
+ PI_CREW_HANDOFF_BUDGET_TOKENS: {
321
+ name: "PI_CREW_HANDOFF_BUDGET_TOKENS",
322
+ parser: "int",
323
+ doc: "est-token (chars/4) budget for the dynamic.dependencyContext prompt layer; beats runtime.handoffBudgetTokens, default 1800; resolved ≤0 or >1_000_000 disables the trim (task-output-context.ts renderDependencyOutputContext)",
324
+ },
320
325
  PI_CREW_MOCK_LIVE_SESSION: {
321
326
  name: "PI_CREW_MOCK_LIVE_SESSION",
322
327
  doc: "'success' mocks a successful live-session (live-session-runtime.ts:558, runtime-resolver.ts:38/116)",
@@ -440,6 +445,39 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
440
445
  name: "PI_CREW_AGENT_EVENTS_PATH",
441
446
  doc: "per-agent events log (<stateRoot>/agents/<taskId>/events.jsonl) written by the surface worker recorder (surface-worker.ts; derived by prepareSurfaceSpawn)",
442
447
  },
448
+ PI_CREW_SESSION_RECOVERY: {
449
+ name: "PI_CREW_SESSION_RECOVERY",
450
+ parser: "boolean",
451
+ doc: "W2 session-file recovery override for killed workers (exitCode null / killed): '0' disables, '1' forces on — beats ChildPiRunInput.sessionRecovery / runtime.sessionRecovery; default ON (session-recovery.ts:resolveSessionRecoveryEnabled)",
452
+ },
453
+ PI_CREW_HERMETIC_WORKERS: {
454
+ name: "PI_CREW_HERMETIC_WORKERS",
455
+ parser: "boolean",
456
+ doc: "R3-19/D5 hermetic worker spawn override: '1' forces --no-extensions on worker argv, '0' restores the old ambient-stack parity (discovered/configured/built-in extensions load); beats BuildPiWorkerArgsInput.hermeticWorkers / runtime.hermeticWorkers; default ON (pi-args.ts:resolveHermeticWorkers). Explicit -e prompt-runtime + agent extensions + --skill flags are unaffected (cli.md:186-191)",
457
+ },
458
+ PI_CREW_WORKER_TRANSPORT: {
459
+ name: "PI_CREW_WORKER_TRANSPORT",
460
+ doc: "W7 EXPERIMENTAL worker transport override: 'rpc' selects the prototype RPC frame-client (src/runtime/rpc/) at the run-worker seam (wired e2728f68; D1/DR2 also plumbs config runtime.workerTransport to the same seam — env WINS over config when set, invalid env fails safe to stdio); anything else/unset → config value, default stdio child-pi path (rpc-worker.ts resolveWorkerTransport)",
461
+ },
462
+ PI_CREW_CLASSIFIER_ENABLED: {
463
+ name: "PI_CREW_CLASSIFIER_ENABLED",
464
+ parser: "boolean",
465
+ doc: "P2-1 (pi 1.0.4 adoption) host-process retry-triage classifier override: '1' enables transient-vs-permanent classification before a queued re-attempt in the child-executor retry loop, '0' forces it off — beats runtime.classifierEnabled; default OFF / dormant (classifier-service.ts:resolveClassifierEnabled). Fallback decision is used on every classifier failure mode, never an error",
466
+ },
467
+ PI_CREW_CLASSIFIER_MODEL: {
468
+ name: "PI_CREW_CLASSIFIER_MODEL",
469
+ doc: "P2-1 classifier model id override ('provider/id', catalog ids only — availability depends on provider credentials at runtime): beats runtime.classifierModel; unset/blank → config value, default 'opencode/jev-1.13-free' (classifier-service.ts:resolveClassifierModel)",
470
+ },
471
+ PI_CREW_RPC_DIALOG_ANSWER: {
472
+ name: "PI_CREW_RPC_DIALOG_ANSWER",
473
+ doc: "W7 EXPERIMENTAL dialog auto-answer policy for RPC-mode extension_ui_request dialogs (select/confirm/input/editor): 'cancel' (default) answers {cancelled:true} — universal safe answer; 'block' leaves the server promise pending (debug). Auto-confirm deliberately NOT implemented (security); invalid → 'cancel' (rpc-worker.ts resolveDialogAnswerPolicy)",
474
+ },
475
+ PI_CREW_RPC_TURN_TIMEOUT_MS: {
476
+ name: "PI_CREW_RPC_TURN_TIMEOUT_MS",
477
+ parser: "int",
478
+ default: "600000",
479
+ doc: "W7/DR1 belt-and-suspenders turn timeout for the EXPERIMENTAL RPC worker transport: max ms a prompt-accepted turn may run without agent_settled before the transport fails with the early-failure shape (DR5 guard still blocks the stdio fallback once a session event was seen). Env-only while RPC stays experimental. Invalid/≤0 → 600000 (rpc-worker.ts resolveRpcTurnTimeoutMs)",
480
+ },
443
481
  PI_CREW_AUTO_EXIT: {
444
482
  name: "PI_CREW_AUTO_EXIT",
445
483
  doc: "'1' → the worker shuts its session down after the final settled turn — spec §5.2 D7 (written by prepareSurfaceSpawn, read by surface-worker.ts)",
@@ -65,6 +65,53 @@ export interface CrewRuntimeConfig {
65
65
  */
66
66
  taskTimeoutMs?: number;
67
67
  inheritContext?: boolean;
68
+ /**
69
+ * W2 (session-file recovery): when a worker dies without a final assistant
70
+ * event (exitCode null / killed), tail-replay its session JSONL (written by
71
+ * the worker under `<artifactsRoot>/sessions/<taskId>/`) and attach the last
72
+ * COMPLETE assistant record to the task result (`recoveredFromSession`).
73
+ * Default: enabled. The env var PI_CREW_SESSION_RECOVERY overrides this
74
+ * flag in either direction (see session-recovery.ts:resolveSessionRecoveryEnabled).
75
+ * Manifest polling is NOT replaced — this only augments the settle-time result.
76
+ */
77
+ sessionRecovery?: boolean;
78
+ /**
79
+ * R3-19/D5 reversal (2026-10-04): hermetic worker spawns — pass
80
+ * `--no-extensions` on worker argv. Explicit `-e prompt-runtime` and agent
81
+ * `extensions:` still load (cli.md:186-187: "Disables discovered, configured,
82
+ * and built-in extensions. Explicit -e paths still load"), and `--skill`
83
+ * flags are independent — what gets cut is the ~1.37s/spawn ambient package
84
+ * stack (pi-crew host ext + configured packages; probe C §R3b.6: 431ms vs
85
+ * 1806ms to first stdout record) plus the ambient tool surface (team/
86
+ * crew_agent/Agent host tools + 8 direct-exposure MCP tools).
87
+ * Default: true. Env PI_CREW_HERMETIC_WORKERS overrides in either
88
+ * direction (pi-args.ts resolveHermeticWorkers). Surface TUI spawns strip
89
+ * the flag — panes keep the full interactive session.
90
+ */
91
+ hermeticWorkers?: boolean;
92
+ /**
93
+ * P2-1 (pi 1.0.4 adoption, 2026-10-07): host-process retry-triage
94
+ * classifier seam (src/runtime/classifier/classifier-service.ts —
95
+ * wrapper around ctx.modelRegistry.classify(), host process only).
96
+ * DORMANT by design: default false, and the consumer (child-executor
97
+ * retry loop) falls back to the pre-existing retry behavior on every
98
+ * classifier failure mode (unavailable / threw / stopReason!=='stop').
99
+ * Enabling opts INTO transient-vs-permanent classification before a
100
+ * queued re-attempt burns a full worker spawn. Env
101
+ * PI_CREW_CLASSIFIER_ENABLED overrides in either direction
102
+ * (resolveClassifierEnabled). Behavior pick, not authority — no
103
+ * sensitive mark.
104
+ */
105
+ classifierEnabled?: boolean;
106
+ /**
107
+ * P2-1: classifier model id ("provider/id" form) for the seam above.
108
+ * Availability is credential-dependent at runtime (getAvailableOfType);
109
+ * a configured-but-uncredentialed id degrades to the fallback decision,
110
+ * never to an error. Default "opencode/jev-1.13-free"
111
+ * (DEFAULT_CLASSIFIER_MODEL); env PI_CREW_CLASSIFIER_MODEL overrides
112
+ * (resolveClassifierModel).
113
+ */
114
+ classifierModel?: string;
68
115
  promptMode?: "replace" | "append";
69
116
  groupJoin?: "off" | "group" | "smart";
70
117
  groupJoinAckTimeoutMs?: number;
@@ -85,6 +132,30 @@ export interface CrewRuntimeConfig {
85
132
  };
86
133
  /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
87
134
  excludeContextBash?: boolean;
135
+ /**
136
+ * Est-token budget for the dynamic.dependencyContext prompt layer
137
+ * (handoff budget). When the rendered dependency output exceeds this many
138
+ * estimated tokens (chars/4 — the same heuristic the prompt breakdown
139
+ * uses), dependencies are trimmed IN DECLARATION ORDER: earlier deps keep
140
+ * their full output, later ones are downgraded to taskId/role/status + a
141
+ * ≤240-char summary head + an artifact pointer
142
+ * (`artifacts/<runId>/results/<taskId>.txt`) the worker can `read` back.
143
+ * Long sharedRead bodies are dropped the same way. Valid range 1..1_000_000;
144
+ * a RESOLVED value ≤0 or >1_000_000 disables the trim entirely
145
+ * (off-switch). Env override PI_CREW_HANDOFF_BUDGET_TOKENS beats this
146
+ * config. Default: 1800.
147
+ */
148
+ handoffBudgetTokens?: number;
149
+ /**
150
+ * W7 (P2-3) EXPERIMENTAL: transport used to spawn child-pi workers.
151
+ * "stdio" (default) — the production print-mode path, unchanged.
152
+ * "rpc" — prototype RPC frame-client (src/runtime/rpc/), wired at the
153
+ * run-worker seam since e2728f68 (D1/DR2 plumbing, 2026-10-06): callers
154
+ * carrying runtimeConfig thread it to the seam. Env
155
+ * PI_CREW_WORKER_TRANSPORT=rpc overrides this config (env wins —
156
+ * established precedence; invalid env fails safe to stdio).
157
+ */
158
+ workerTransport?: "stdio" | "rpc";
88
159
  /**
89
160
  * Mux-surface policy (mux-surface spec v0.7 §8.1): WHERE worker processes
90
161
  * live — a pane in tmux/herdr or headless child processes. Surface only
@@ -92,6 +92,15 @@ function startForegroundRunImpl(
92
92
  runId?: string,
93
93
  ): void {
94
94
  const ownerGeneration = ctx.captureSessionGeneration();
95
+ // Issue #62: tool-dispatched runs receive a SPREAD COPY of the session
96
+ // context (withSessionId() always returns { ...ctx }), so the object-
97
+ // identity check (isContextCurrent) was false from the very first moment
98
+ // and the whole completion block below was silently skipped for every
99
+ // foreground run started via the team/subagent tools — stale working
100
+ // message, no crew:run-completed entry, no crew.run.* metrics. Liveness
101
+ // for the reporting half is therefore session-ID based; the UI clear is
102
+ // unconditional (try/catch guards a disposed ctx).
103
+ const ownerSessionId = extensionCtx.sessionManager?.getSessionId?.();
95
104
  const controller = new AbortController();
96
105
  const key = runId ?? Symbol();
97
106
  ctx.foregroundTeamRunControllers.set(key, controller);
@@ -130,7 +139,7 @@ function startForegroundRunImpl(
130
139
  logInternalError("register.foreground-run-failure", statusError, `runId=${runId}`);
131
140
  }
132
141
  }
133
- if (ctx.isContextCurrent(extensionCtx, ownerGeneration)) {
142
+ if (ctx.isOwnerSessionCurrent(ownerGeneration, ownerSessionId)) {
134
143
  extensionCtx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
135
144
  } else {
136
145
  logInternalError("register.foreground-run-failure", error, `runId=${runId} context disposed`);
@@ -145,18 +154,18 @@ function startForegroundRunImpl(
145
154
  })
146
155
  .catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
147
156
  }
148
- const ownerCurrent = ctx.isContextCurrent(extensionCtx, ownerGeneration);
149
- // The owner ctx can go stale while the run is in flight (e.g. the
150
- // user opened an agent session view via /crew-view and the run
151
- // finishes there): the `hasUI` getter THROWS on a stale ctx, so
152
- // it must only be touched for the still-current owner.
153
- if (ownerCurrent && extensionCtx.hasUI) {
154
- try {
157
+ const ownerCurrent = ctx.isOwnerSessionCurrent(ownerGeneration, ownerSessionId);
158
+ // Issue #62: clear the working message UNCONDITIONALLY. The `hasUI`
159
+ // getter THROWS on a disposed ctx, so touch it inside try/catch —
160
+ // a stale ctx just means the host already reset its UI; a live one
161
+ // must never keep spinning a finished run's label.
162
+ try {
163
+ if (extensionCtx.hasUI) {
155
164
  setWorkingIndicator(extensionCtx);
156
165
  extensionCtx.ui.setWorkingMessage();
157
- } catch {
158
- /* ignore */
159
166
  }
167
+ } catch {
168
+ /* disposed ctx — host-side UI reset already happened */
160
169
  }
161
170
  if (ownerCurrent && runId) {
162
171
  const loaded = loadRunManifestById(extensionCtx.cwd, runId);