pi-crew 0.11.2 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/dist/index.mjs +491 -493
- package/package.json +5 -2
- package/skills/real-test-pi-crew/SKILL.md +19 -11
- package/src/extension/async-notifier.ts +32 -8
- package/src/extension/crew-input-router.ts +9 -2
- package/src/extension/plan-orchestrate.ts +19 -3
- package/src/extension/registration/commands/shared.ts +11 -10
- package/src/extension/registration/foreground-run-controller.ts +2 -2
- package/src/extension/registration/lazy-configurers.ts +4 -1
- package/src/extension/registration/lifecycle-handlers.ts +16 -10
- package/src/extension/registration/lifecycle.ts +2 -0
- package/src/extension/registration/observability.ts +27 -18
- package/src/extension/registration/team-tool.ts +9 -2
- package/src/extension/registration/wire-cross-extension.ts +2 -0
- package/src/extension/team-manager-command.ts +3 -2
- package/src/extension/team-tool/goal-wrap.ts +7 -0
- package/src/extension/team-tool/goal.ts +12 -0
- package/src/extension/team-tool/parallel-dispatch.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +1 -1
- package/src/extension/team-tool/run.ts +1 -1
- package/src/runtime/async-runner.ts +36 -0
- package/src/runtime/broker/crew-broker.ts +61 -62
- package/src/runtime/broker/delegate/delegate-event.ts +12 -8
- package/src/runtime/broker/protocol/connection-state.ts +10 -0
- package/src/runtime/broker/protocol/lock-busy.ts +76 -0
- package/src/runtime/delegate-spawn.ts +16 -2
- package/src/runtime/deterministic-ast.ts +14 -2
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +78 -58
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +14 -2
- package/src/runtime/goal-workflow/goal-loop-runner.ts +32 -0
- package/src/runtime/group-join.ts +11 -3
- package/src/runtime/live-session/live-session-runtime.ts +1 -1
- package/src/runtime/model/runtime-warmup.ts +2 -0
- package/src/runtime/output/output-validator.ts +10 -5
- package/src/runtime/plan-approval.ts +14 -1
- package/src/runtime/recovery/crash-recovery.ts +75 -52
- package/src/runtime/subagent-manager.ts +43 -13
- package/src/runtime/task-runner/child-executor.ts +17 -6
- package/src/runtime/task-runner/post-execution.ts +44 -14
- package/src/runtime/task-runner/prompt-builder.ts +8 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +36 -14
- package/src/runtime/task-runner.ts +7 -0
- package/src/runtime/team-runner.ts +4 -1
- package/src/runtime/verification/verification-worktree.ts +3 -0
- package/src/state/coordination/mailbox.ts +7 -2
- package/src/state/stores/state-store.ts +8 -1
- package/src/ui/dashboard-panes/agents-pane.ts +11 -8
- package/src/ui/inline-panel/index.ts +1 -1
- package/src/ui/render-diff.ts +19 -6
- package/src/ui/run-dashboard.ts +11 -1
- package/src/ui/run-snapshot-cache.ts +9 -3
- package/src/ui/widget/task-list.ts +8 -5
- package/src/ui/widget/widget-model.ts +3 -1
- package/tsconfig.json +2 -0
- package/src/ui/card-colors.ts +0 -168
- package/src/utils/completion-dedupe.ts +0 -63
- package/src/utils/resolve-shell.ts +0 -36
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.4",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
7
10
|
"engines": {
|
|
8
11
|
"node": ">=22.0.0"
|
|
9
12
|
},
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
"test:functional": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=120000 test/functional/pi-crew-functional.test.ts",
|
|
94
97
|
"test:platform": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=60000 test/platform/*.test.ts",
|
|
95
98
|
"build:bundle": "node scripts/build-bundle.mjs",
|
|
96
|
-
"test:bundle": "node
|
|
99
|
+
"test:bundle": "node scripts/test-runner.mjs --test-timeout=60000 --test-force-exit test/unit/bundle-load.test.ts",
|
|
97
100
|
"watch:bundle": "node scripts/watch-bundle.mjs",
|
|
98
101
|
"build:bundle:once": "node scripts/watch-bundle.mjs --once",
|
|
99
102
|
"dev": "node scripts/dev-runner.mjs",
|
|
@@ -119,6 +119,8 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
|
|
|
119
119
|
| `workflows/*.workflow.md` | T7 |
|
|
120
120
|
| `.github/workflows/ci.yml` | T11e |
|
|
121
121
|
| `scripts/wc-gate.mjs` | T11b |
|
|
122
|
+
| `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 |
|
|
123
|
+
| `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 |
|
|
122
124
|
| `dist/` (bundle) | T3 → T4 → T8 → T11j |
|
|
123
125
|
| bất kỳ đường dẫn trên, trước khi commit | gates đầy đủ: critical + unit + tsc + biome + md5 |
|
|
124
126
|
|
|
@@ -134,6 +136,8 @@ End-to-end verification discipline for pi-crew changes. Distilled from the broke
|
|
|
134
136
|
|
|
135
137
|
> **2026-09-26 update (ambient-noise triage + temp-workspace hygiene + inline-async seam, commits `73790eaa`→`0006ddd5`, reports `real-test-2026-09-26-full-battery-manual.md` + addenda 1-5, ADRs `2026-09-26-inline-async-test-seam.md` / `2026-09-26-temp-workspace-hygiene.md`)**: (1) **Guest `gc-*` skip** — delegate-owned guest tasks have NO heartbeat channel; watcher + aggregator skip them at two independent layers (`agent==="delegate"` marker), guard-tests prove a genuinely-missing REAL worker is still flagged. (2) **Reconciler hygiene** — sweep batch rotates statelessly (`floor(now/60s) % batches`, starvation từ cluster kẹt đầu alphabet đã chết), `.cleanup-in-progress` sentinels cũ >10 phút được reclaim, test-runner dọn `pi-crew-*` pre-existing (mtime < suiteStart−30') sau mỗi suite — battery probe dirs trong /tmp phải KHÔNG đặt tên `pi-crew-*` hoặc phải tạo <30' trước T11. (3) **Inline-async test seam** — `PI_CREW_TEST_ASYNC_INLINE=1` + `PI_CREW_ALLOW_MOCK=1` chạy async run IN-PROCESS (`executeBackgroundRun` core shared với runner detached; `main()` giờ entry-guarded bằng `PI_CREW_BACKGROUND_RUNNER_ENTRY` hoặc argv check). Seam là công cụ UNIT TEST cho heavy async files (subagent-tools-integration: 5+min stall-prone → 14/14 trong 37s) — **battery T7/T9b/T10 TUYỆT ĐỐI không set** (xem row troubleshooting "seam leak"). New pinned suites: `stale-reconciler-rotation` (mutation-checked), `stale-reconciler-sentinel-reclaim`, `sweep-test-tmp` ×2, `async-runner-inline-seam` (mutation-checked), `heartbeat-watcher-guest-skip` ×2, `heartbeat-overlay` guest tests ×2. Suite anchors @ 2026-09-26: `npm test` **EXIT=0** — 8379 tests / 8372 pass / 0 fail / 7 skipped (~14 min), `test:critical` 116 (~25s), bundle **1677.8 KB** md5 `3860c0264940e7df91511da493fa375e`; CI flake Windows `subagent-tools-integration` spawn-stall **RESOLVED** bởi seam (đã từng tốn rerun ×4 trong một ngày).
|
|
136
138
|
|
|
139
|
+
> **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`.
|
|
140
|
+
|
|
137
141
|
> **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.
|
|
138
142
|
>
|
|
139
143
|
> **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).
|
|
@@ -194,14 +198,16 @@ The skill maps to existing CI gates as follows:
|
|
|
194
198
|
| `npm run typecheck` | Tier 3 | `.github/workflows/*.yml` (every PR) |
|
|
195
199
|
| `npm run check:wc-gate` | Tier 11b | **in BOTH `ci` and `ci:fast` scripts** (`package.json:71-72`) + explicit step in `.github/workflows/ci.yml:66-71` (since `09dda842` — was `ci:fast`-only, i.e. advisory) |
|
|
196
200
|
| 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 |
|
|
197
|
-
| `npm run test:bundle` (bundle import smoke, 2 tests) | Tier 3 post-build sanity | `test/unit/bundle-load.test.ts` |
|
|
201
|
+
| `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) |
|
|
198
202
|
| `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 |
|
|
199
|
-
| Full `npm test` (= unit
|
|
203
|
+
| 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 |
|
|
200
204
|
| `PI_CREW_SMOKE=1` env | Tier 11e | set ONLY in `weekly-smoke.yml` (auth-gated); nightly.yml deliberately does NOT (comment at `:24`) |
|
|
201
205
|
| `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/` |
|
|
202
206
|
| `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 |
|
|
203
207
|
| `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) |
|
|
204
|
-
| `
|
|
208
|
+
| `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` |
|
|
209
|
+
| `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) |
|
|
210
|
+
| `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) |
|
|
205
211
|
|
|
206
212
|
To add Tier 1 to a pre-commit hook:
|
|
207
213
|
|
|
@@ -231,7 +237,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
231
237
|
|
|
232
238
|
**What**: run the curated 14-file fast subset.
|
|
233
239
|
|
|
234
|
-
**Why this exists**: full `npm run test:unit` runs
|
|
240
|
+
**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.
|
|
235
241
|
|
|
236
242
|
**How**:
|
|
237
243
|
|
|
@@ -239,7 +245,7 @@ To add Tier 1 to CI as a fast-feedback gate (under 30s):
|
|
|
239
245
|
time npm run test:critical
|
|
240
246
|
```
|
|
241
247
|
|
|
242
|
-
Expected output: `# tests 116 # pass 116 # fail 0 # duration_ms ~
|
|
248
|
+
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.
|
|
243
249
|
|
|
244
250
|
**References**:
|
|
245
251
|
|
|
@@ -303,7 +309,7 @@ All three must show `# pass 101 # fail 0`. Measured times in this session (2026-
|
|
|
303
309
|
**How**:
|
|
304
310
|
|
|
305
311
|
```bash
|
|
306
|
-
npm run typecheck # ~20s, exits 0 with "strip-types import ok"
|
|
312
|
+
npm run typecheck # ~3.5s warm / ~5s cold since RR-021 incremental tsc (was ~20s), exits 0 with "strip-types import ok"
|
|
307
313
|
npm run build:bundle # <1s, prints "[build-bundle] dist/index.mjs NNNN KB in NNN ms"
|
|
308
314
|
md5sum dist/index.mjs
|
|
309
315
|
node scripts/check-bundle-staleness.mjs # ARCH-7: staleness + path-leak scan — exit 0
|
|
@@ -320,7 +326,7 @@ Compare the printed md5 against what the user's Pi session loaded. If they diffe
|
|
|
320
326
|
| Bundle builder | `scripts/build-bundle.mjs` (esbuild-based, bundles `index.bundle.ts` → `dist/index.mjs`) |
|
|
321
327
|
| 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** |
|
|
322
328
|
| Postinstall hook | `scripts/postinstall.mjs:43` — best-effort bundle rebuild; falls back to strip-types if esbuild missing |
|
|
323
|
-
| 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
|
|
329
|
+
| 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` |
|
|
324
330
|
|
|
325
331
|
---
|
|
326
332
|
|
|
@@ -776,9 +782,9 @@ herdr chỉ được detect khi **chính pi session đang chạy trong một her
|
|
|
776
782
|
grep -c "atomicWriteJson" src/state/stores/plan-store.ts src/state/stores/ownership-map.ts # >=1 each
|
|
777
783
|
# 2. terminal-state events are sync appendEvent (crash-recovery design comment at :221
|
|
778
784
|
# (file lives at src/runtime/recovery/crash-recovery.ts after the runtime reorg)
|
|
779
|
-
grep -n "Log the event first" src/runtime/recovery/crash-recovery.ts # design intent: sync
|
|
785
|
+
grep -n "Log the event first" src/runtime/recovery/crash-recovery.ts # design intent: sync (:228 @ 2026-09-29; was :221 pre-RR-021)
|
|
780
786
|
# 3. buffered-site census — snapshot & audit:
|
|
781
|
-
grep -rln "appendEventBuffered" src/ | wc -l #
|
|
787
|
+
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
|
|
782
788
|
# 4. the full gate — test:critical has NO stores/dwf/recovery coverage:
|
|
783
789
|
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
|
|
784
790
|
```
|
|
@@ -1080,7 +1086,7 @@ PY
|
|
|
1080
1086
|
| 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`) |
|
|
1081
1087
|
| Source edit seen immediately | No, requires bundle rebuild + reload | n/a (permanent) | `index.ts:1-25` — bundle resolution rules |
|
|
1082
1088
|
| Skip disabled-path proof | `effectiveEnabled()` regression slips through | n/a (permanent) | Tier 2 above |
|
|
1083
|
-
| `npm run test:unit` against the full suite (
|
|
1089
|
+
| `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 |
|
|
1084
1090
|
| Skip typecheck | TS errors slip past `test:critical` (which uses `--test-timeout=30000`) | n/a (permanent) | Tier 3 above |
|
|
1085
1091
|
| Run `pi` from a stale bundle | Session shows old behavior despite src/ edits | n/a (permanent) | `scripts/check-bundle-staleness.mjs` — CI gate |
|
|
1086
1092
|
| Test by reading code | Proves nothing about runtime | n/a (permanent) | All tiers above |
|
|
@@ -1109,7 +1115,7 @@ PY
|
|
|
1109
1115
|
| **Worker killed mid long-silent-bash** (Batch-1 postmortem): one >5–10 min command emits no output events → watchdog SIGTERM mid-run — exit 143 WHILE the command runs. Work was intact; manual re-run green — the kill WAS the "hang". **2026-09-20 sharpening**: the trigger is EVENT SILENCE, not duration — `\| tail`/`\| head`/`> file` buffering manufactured silence from a chatty suite (proven: `worker.response_timeout` at exactly cmd_start+600s, heartbeat fresh, 29 earlier streaming commands all survived); genuinely-silent commands die unpiped too. | n/a (quirk — `CONTEXT.md` Flagged #1; P2 candidate) | Never `\| tail` a long suite in a worker — stream or split it; if buffering stdout, keep stderr out of the pipe (`2>&1` removes the last update source); raise `PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS` for legit silent workloads. On exit-143-mid-command: re-run manually BEFORE diagnosing a code bug; mine `.crew/state/runs/<runId>/background.log` for the killed worker's findings. Postmortem: `postmortem-batch-1-sigterm.md`; full RCA: run `team_20260919150900_6af9ee2bcd57a7f3` |
|
|
1110
1116
|
| **`pkill -f "test:unit"` kills your own shell** (2026-09-16): the pattern matches the `bash -c` cmdline of the tool call that runs it, so the command dies before it starts the replacement suite — and the log file simply never appears. | n/a (self-inflicted) | Kill by pid (`pgrep -f test-runner` → `kill <pid>`), or match a pattern the sink does not contain | Tier 13 "How" step 0 — detached start: `(setsid nohup npm run test:unit > /tmp/suite.log 2>&1 < /dev/null &)` |
|
|
1111
1117
|
| **`pgrep -f test-runner` read as liveness** (2026-09-16): the same self-match makes it report `RUNNING` forever, so a suite that died 10 minutes ago looks alive and the "result" you read is a truncated log. | n/a (self-inflicted) | Liveness = `ps -eo pid,etime,cmd \| grep -E "test-runner\|node --test"` with the grep itself excluded | Tier 13 harness step 1 — a run is DONE only when the `# tests/# pass/# fail` block exists at EOF |
|
|
1112
|
-
| **Reading a truncated suite log as a summary** (2026-09-16): the runner died with `Test runner error: spawnSync … ETIMEDOUT` at subtest 3946/7860 under load; the last numbers looked like a verdict. | n/a (runner) | The verdict is the `# tests/# pass/# fail` block at EOF — absent means the run did not finish; re-run in the foreground with nothing else heavy running. Since 2026-09-17 (F05) the runner itself fails CLOSED on this class — `resolveExitCode()` (`scripts/test-runner.mjs:
|
|
1118
|
+
| **Reading a truncated suite log as a summary** (2026-09-16): the runner died with `Test runner error: spawnSync … ETIMEDOUT` at subtest 3946/7860 under load; the last numbers looked like a verdict. | n/a (runner) | The verdict is the `# tests/# pass/# fail` block at EOF — absent means the run did not finish; re-run in the foreground with nothing else heavy running. Since 2026-09-17 (F05) the runner itself fails CLOSED on this class — `resolveExitCode()` (`scripts/test-runner.mjs:45`) maps ETIMEDOUT/spawn-error/signal-kill to a non-zero exit, so wrappers/CI see red instead of a silent exit-0 false green; the spawn deadline also rose 900s→1500s (`PI_CREW_TEST_RUNNER_TIMEOUT_MS` override). RR-021 rounds 2-3 closed the LAST two silent-green classes in the wrapper: `# cancelled N>0` (TAP dual-reporter scrape, fails both single-run and batch paths) and explicitly-listed missing files (pre-check exits 1 naming paths — node:test v22 used to silently skip them) | Failure symptoms row "Test runner error: spawnSync … ETIMEDOUT" |
|
|
1113
1119
|
| **Running the full suite on a tree you are still editing** (2026-09-16): a 12-minute suite started before the last fixes reports failures that no longer exist — and passes fixes that were not in yet. | n/a (process) | Freeze the tree first; if you must edit, the run is VOID — say so instead of quoting its numbers | Done-criteria: "Full `test:unit` fresh-run" |
|
|
1114
1120
|
| **Grepping the bundle without context or escape-awareness** (2026-09-16): esbuild escapes non-ASCII as `\u250F` (uppercase hex) so glyph greps miss, while `"->"` (the progress **wire format**, `roleSeparator`) and `Crew agents` (error message + task-graph markdown) are permanent false positives. | n/a (permanent) | Grep the codepoint escape (`u250F`, `u258F`) and always print ±90 chars of context before concluding | Tier 3 bundle check + Tier 13 step 2 (escape-aware proof snippet) |
|
|
1115
1121
|
| **Trusting a delegated UI claim** (2026-09-16): four parallel executors + a verifier reported "dedupe applied" and "hints canonical"; the rendered surfaces still showed `fast-fix/fast-fix` and an invented `· right default`. Summaries describe intent. | n/a (process) | Render the surface with REAL state before believing any UI claim — including your own | Tier 13 (this entire tier exists for this class) |
|
|
@@ -1153,6 +1159,8 @@ When a tier fails, the recovery is usually quick. Match the symptom to the cause
|
|
|
1153
1159
|
| 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 |
|
|
1154
1160
|
| 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 |
|
|
1155
1161
|
| 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 |
|
|
1162
|
+
| `[test-runner] FAIL: N cancelled test(s)` (runner diagnostic, any suite size) | A test drains the event loop while pending (unref'd race timers — the RR-021 round-1 shape) — node:test cancels it and the runner now fails loudly instead of exiting 0 | Fix the TEST (ref the timer / gate-promise pattern), never ignore or re-run hoping it passes; `test-runner-exit.test.ts` has the canonical red-green fixture |
|
|
1163
|
+
| `[test-runner] FAIL: N explicitly-listed test file(s) do not exist` | An explicit-list script (`test:critical` & friends) or manual command references a renamed/deleted file — node:test v22 silently skipped it (exit 0); the RR-021 round-3 pre-check refuses to spawn | Update the invoking script/command — the named path is the file that moved. Glob args are exempt (node:test expands them) |
|
|
1156
1164
|
| Full `test:unit` fails ONLY on `wait-request-broker.test.ts` under parallel load | Per-file 180s runner timeout vs the file's real runtime (passes isolated) | `node scripts/test-runner.mjs test/unit/runtime/broker/wait-request-broker.test.ts` — green in isolation = infra flake, not a regression |
|
|
1157
1165
|
| `undefined` (or `undefined — …`) painted in a live surface | an optional segment interpolated into a template literal without a guard. **Check BOTH paths**: the pure builder and the component that composes the same row (2026-09-16: `widget-renderer.buildWidgetLines` guarded it, `src/ui/widget/index.ts` did not). | Guard before composing (`if (!x) return [] / return undefined`), add a regression lock that renders the zero-state through the COMPONENT, and sweep every `${optional}` on the surface. |
|
|
1158
1166
|
| Spinner keeps spinning after the run finished (`⠹ … 0 running`) | the surface hardcodes a spinner frame instead of deriving the glyph from state | Derive it: spinner only while an agent/run is actually running, otherwise the outcome glyph (`✓`/`✗`). Assert in the battery that "0 running" never coexists with a braille glyph. |
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readCrewAgents, saveCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
3
3
|
import { checkProcessLiveness, isActiveRunStatus } from "../runtime/process-status.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { withRunLock } from "../state/coordination/locks.ts";
|
|
5
5
|
import { appendEvent, readEventsCursor, type TeamEvent } from "../state/event-log/event-log.ts";
|
|
6
6
|
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/stores/state-store.ts";
|
|
7
7
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
8
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
9
|
import { extractSessionId } from "../utils/session-utils.ts";
|
|
10
|
-
import { listRuns } from "./run-index.ts";
|
|
10
|
+
import { listRecentRuns, listRuns } from "./run-index.ts";
|
|
11
11
|
import type { WebhookNotifier } from "./webhook-notify.ts";
|
|
12
12
|
|
|
13
13
|
export interface AsyncNotifierState {
|
|
@@ -107,7 +107,11 @@ function markActiveTasksAndAgentsFailed(run: TeamRunManifest, message: string):
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export function markDeadAsyncRunIfNeeded(
|
|
110
|
+
export async function markDeadAsyncRunIfNeeded(
|
|
111
|
+
run: TeamRunManifest,
|
|
112
|
+
now = Date.now(),
|
|
113
|
+
quietMs = 30_000,
|
|
114
|
+
): Promise<TeamRunManifest | undefined> {
|
|
111
115
|
if (!run.async || !isActiveRunStatus(run.status)) return undefined;
|
|
112
116
|
const liveness = checkProcessLiveness(run.async.pid);
|
|
113
117
|
if (liveness.alive) return undefined;
|
|
@@ -116,8 +120,13 @@ export function markDeadAsyncRunIfNeeded(run: TeamRunManifest, now = Date.now(),
|
|
|
116
120
|
if (latestEventAgeMs(events, now) < quietMs) return undefined;
|
|
117
121
|
const asyncPid = run.async.pid;
|
|
118
122
|
const message = `Background runner died unexpectedly; check background.log (${liveness.detail}).`;
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
// RR-021 WI-1.4: rerouted to the async run-lock (same v0.9.26 lock family as
|
|
124
|
+
// the sync helper — sync and async acquisitions interoperate via live-token
|
|
125
|
+
// registration, RR-011 F02). The notifier tick is async, so we must not hold
|
|
126
|
+
// the event loop with a blocking sync acquisition while other async contenders
|
|
127
|
+
// (background runner, stale reconciler) wait.
|
|
128
|
+
return withRunLock(run, async () => {
|
|
129
|
+
const fresh = loadRunManifestById(run.cwd, run.runId); // NOTE: best-effort only inside the lock; concurrent writes may cause inconsistency;
|
|
121
130
|
if (!fresh || !isActiveRunStatus(fresh.manifest.status)) return undefined;
|
|
122
131
|
const failed = updateRunStatus(fresh.manifest, "failed", message);
|
|
123
132
|
markActiveTasksAndAgentsFailed(failed, message);
|
|
@@ -163,16 +172,25 @@ export function startAsyncRunNotifier(
|
|
|
163
172
|
if (isFinished(run.status) && updatedAtMs < staleBeforeMs) addSeenFinishedRunId(state, run.runId);
|
|
164
173
|
}
|
|
165
174
|
let cachedRuns: TeamRunManifest[] | undefined;
|
|
166
|
-
|
|
175
|
+
// RR-021 WI-1.4: the tick is async now (markDeadAsyncRunIfNeeded awaits the
|
|
176
|
+
// run-lock). `ticking` preserves the old no-overlap semantics — setInterval
|
|
177
|
+
// must not re-enter a tick that is still awaiting a lock.
|
|
178
|
+
let ticking = false;
|
|
179
|
+
const tick = async (): Promise<void> => {
|
|
180
|
+
if (ticking) return;
|
|
181
|
+
ticking = true;
|
|
167
182
|
try {
|
|
168
183
|
if (options.isCurrent && !options.isCurrent(generation)) return;
|
|
169
184
|
const nowMs = Date.now();
|
|
170
185
|
if (cachedRuns === undefined || nowMs - (state.lastListRunsMs ?? 0) > LIST_RUNS_DEBOUNCE_MS) {
|
|
171
|
-
|
|
186
|
+
// RR-021 WI-4.1: bounded recent-runs read (40 = 2x the 20 we keep) instead
|
|
187
|
+
// of a full listRuns() index scan on every debounce window — the 2x bound
|
|
188
|
+
// survives the ownsRun filter still yielding 20 owned runs.
|
|
189
|
+
cachedRuns = listRecentRuns(ctx.cwd, 40).filter(ownsRun).slice(0, 20);
|
|
172
190
|
state.lastListRunsMs = nowMs;
|
|
173
191
|
}
|
|
174
192
|
for (const run of cachedRuns) {
|
|
175
|
-
const current = markDeadAsyncRunIfNeeded(run) ?? run;
|
|
193
|
+
const current = (await markDeadAsyncRunIfNeeded(run)) ?? run;
|
|
176
194
|
if (!isFinished(current.status) || state.seenFinishedRunIds.has(current.runId)) continue;
|
|
177
195
|
addSeenFinishedRunId(state, current.runId);
|
|
178
196
|
// Suppress notifications for INTERNAL goal-loop sub-runs.
|
|
@@ -213,7 +231,13 @@ export function startAsyncRunNotifier(
|
|
|
213
231
|
return;
|
|
214
232
|
}
|
|
215
233
|
logInternalError("async-notifier", error, `interval=${intervalMs}`);
|
|
234
|
+
} finally {
|
|
235
|
+
ticking = false;
|
|
216
236
|
}
|
|
237
|
+
};
|
|
238
|
+
state.interval = setInterval(() => {
|
|
239
|
+
// tick() never rejects (it catches internally) — void is safe.
|
|
240
|
+
void tick();
|
|
217
241
|
}, intervalMs);
|
|
218
242
|
// Defense-in-depth: never let the notifier timer keep the event loop alive.
|
|
219
243
|
// If stopAsyncRunNotifier is missed (session switch race), the next run of
|
|
@@ -55,6 +55,13 @@ function phraseToRegex(phrase: string): RegExp {
|
|
|
55
55
|
return new RegExp(`^${escaped}\\b`, "i");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// RR-021 WI-4.3k: precompile the per-phrase regexes ONCE — the router used to
|
|
59
|
+
// rebuild every phrase regex on every routed input line.
|
|
60
|
+
const CREW_PHRASE_MATCHERS: ReadonlyArray<{ command: string; regex: RegExp }> = CREW_PHRASES.map((entry) => ({
|
|
61
|
+
command: entry.command,
|
|
62
|
+
regex: phraseToRegex(entry.phrase),
|
|
63
|
+
}));
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Try to rewrite a natural-language crew phrase into a slash command.
|
|
60
67
|
* Returns the rewritten command string, or `null` if no rule matches.
|
|
@@ -67,8 +74,8 @@ export function rewriteCrewInput(text: string): string | null {
|
|
|
67
74
|
// Never transform explicit slash commands or inputs that don't start with
|
|
68
75
|
// a crew/team keyword phrase.
|
|
69
76
|
if (trimmed.startsWith("/")) return null;
|
|
70
|
-
for (const entry of
|
|
71
|
-
const match = trimmed.match(
|
|
77
|
+
for (const entry of CREW_PHRASE_MATCHERS) {
|
|
78
|
+
const match = trimmed.match(entry.regex);
|
|
72
79
|
if (!match) continue;
|
|
73
80
|
// Carry any remaining args after the matched phrase forward.
|
|
74
81
|
const rest = trimmed.slice(match[0].length).trim();
|
|
@@ -183,11 +183,27 @@ export function parsePlanDocumentSimple(planPath: string): OrchestratedStep[] {
|
|
|
183
183
|
/**
|
|
184
184
|
* Detect implicit tag from content keywords when no explicit tag is present.
|
|
185
185
|
*/
|
|
186
|
+
// RR-021 WI-4.3k: precompiled word-boundary regexes — hasWord used to compile
|
|
187
|
+
// a fresh RegExp for every word on every call.
|
|
188
|
+
const WORD_MATCHERS: Readonly<Record<string, RegExp>> = {
|
|
189
|
+
design: /\bdesign\b/,
|
|
190
|
+
architecture: /\barchitecture\b/,
|
|
191
|
+
implement: /\bimplement\b/,
|
|
192
|
+
coding: /\bcoding\b/,
|
|
193
|
+
security: /\bsecurity\b/,
|
|
194
|
+
audit: /\baudit\b/,
|
|
195
|
+
build: /\bbuild\b/,
|
|
196
|
+
compile: /\bcompile\b/,
|
|
197
|
+
test: /\btest\b/,
|
|
198
|
+
verify: /\bverify\b/,
|
|
199
|
+
review: /\breview\b/,
|
|
200
|
+
feedback: /\bfeedback\b/,
|
|
201
|
+
};
|
|
186
202
|
function detectImplicitTag(content: string): string | undefined {
|
|
187
203
|
const lowerContent = content.toLowerCase();
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
const hasWord = (word:
|
|
204
|
+
// Word-boundary match avoids substring false matches: "implementation"
|
|
205
|
+
// must NOT trigger "impl" — only the word "implement" should.
|
|
206
|
+
const hasWord = (word: keyof typeof WORD_MATCHERS): boolean => WORD_MATCHERS[word].test(lowerContent);
|
|
191
207
|
|
|
192
208
|
if (hasWord("design") || hasWord("architecture")) return "design";
|
|
193
209
|
if (hasWord("implement") || hasWord("coding")) return "impl";
|
|
@@ -46,6 +46,7 @@ export async function handleTeamTool(
|
|
|
46
46
|
if (!_cachedHandleTeamTool) {
|
|
47
47
|
if (!_handleTeamToolPromise) {
|
|
48
48
|
_handleTeamToolPromise = import("../../team-tool.ts").then((mod) => {
|
|
49
|
+
// LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
49
50
|
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
50
51
|
return mod.handleTeamTool;
|
|
51
52
|
});
|
|
@@ -109,13 +110,13 @@ export async function ui(): Promise<NonNullable<typeof _uiCache>> {
|
|
|
109
110
|
if (!_uiCachePromise) {
|
|
110
111
|
_uiCachePromise = (async () => {
|
|
111
112
|
const [rd, tv, co, md, mc, ap, ma] = await Promise.all([
|
|
112
|
-
import("../../../ui/run-dashboard.ts"),
|
|
113
|
-
import("../../../ui/transcript-viewer.ts"),
|
|
114
|
-
import("../../../ui/overlays/confirm-overlay.ts"),
|
|
115
|
-
import("../../../ui/overlays/mailbox-detail-overlay.ts"),
|
|
116
|
-
import("../../../ui/overlays/mailbox-compose-overlay.ts"),
|
|
117
|
-
import("../../../ui/overlays/agent-picker-overlay.ts"),
|
|
118
|
-
import("../../../ui/mascot.ts"),
|
|
113
|
+
import("../../../ui/run-dashboard.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
114
|
+
import("../../../ui/transcript-viewer.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
115
|
+
import("../../../ui/overlays/confirm-overlay.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
116
|
+
import("../../../ui/overlays/mailbox-detail-overlay.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
117
|
+
import("../../../ui/overlays/mailbox-compose-overlay.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
118
|
+
import("../../../ui/overlays/agent-picker-overlay.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
119
|
+
import("../../../ui/mascot.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
119
120
|
]);
|
|
120
121
|
const cache = {
|
|
121
122
|
RunDashboard: rd.RunDashboard,
|
|
@@ -249,9 +250,9 @@ export function teamCommandContext(ctx: ExtensionCommandContext): ExtensionComma
|
|
|
249
250
|
export async function openTeamSettingsOverlay(ctx: ExtensionContext): Promise<void> {
|
|
250
251
|
if (!ctx.hasUI) return;
|
|
251
252
|
const [{ updateConfig, parseConfig }, { asCrewTheme }, { createSettingsOverlay }] = await Promise.all([
|
|
252
|
-
import("../../../config/config.ts"),
|
|
253
|
-
import("../../../ui/theme-adapter.ts"),
|
|
254
|
-
import("../../../ui/settings-overlay.ts"),
|
|
253
|
+
import("../../../config/config.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
254
|
+
import("../../../ui/theme-adapter.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
255
|
+
import("../../../ui/settings-overlay.ts"), // LAZY: defer heavy command/UI chain until first slash-command (blind-spot #2)
|
|
255
256
|
]);
|
|
256
257
|
const loaded = loadConfig(ctx.cwd);
|
|
257
258
|
const config = loaded.config as Record<string, unknown>;
|
|
@@ -103,7 +103,7 @@ function startForegroundRunImpl(
|
|
|
103
103
|
extensionCtx.ui.setWorkingMessage(runId ? `pi-crew foreground run ${runId}...` : "pi-crew foreground run...");
|
|
104
104
|
}
|
|
105
105
|
if (runId) {
|
|
106
|
-
void import("../../runtime/foreground-watchdog.ts")
|
|
106
|
+
void import("../../runtime/foreground-watchdog.ts") // LAZY: defer watchdog module until first foreground run
|
|
107
107
|
.then(({ startForegroundWatchdog }) => {
|
|
108
108
|
startForegroundWatchdog({ pi, cwd: extensionCtx.cwd, runId });
|
|
109
109
|
})
|
|
@@ -139,7 +139,7 @@ function startForegroundRunImpl(
|
|
|
139
139
|
.finally(() => {
|
|
140
140
|
ctx.foregroundTeamRunControllers.delete(key);
|
|
141
141
|
if (runId) {
|
|
142
|
-
void import("../../runtime/foreground-watchdog.ts")
|
|
142
|
+
void import("../../runtime/foreground-watchdog.ts") // LAZY: defer watchdog module until first foreground run
|
|
143
143
|
.then(({ stopWatchdog }) => {
|
|
144
144
|
stopWatchdog(runId);
|
|
145
145
|
})
|
|
@@ -67,7 +67,10 @@ function installTurnReconcileHook(pi: ExtensionAPI, ctx: RegistrationContext): v
|
|
|
67
67
|
const current = ctx.currentCtx;
|
|
68
68
|
if (!current || ctx.cleanedUp) return;
|
|
69
69
|
try {
|
|
70
|
-
|
|
70
|
+
// RR-021 WI-1.5: async reconcile — fire-and-forget from the sync hook.
|
|
71
|
+
void reconcileAllStaleRuns(current.cwd, ctx.getManifestCache(current.cwd), undefined, extractSessionId(current)).catch(
|
|
72
|
+
(error) => logInternalError("register.autoRepair.turnHook", error),
|
|
73
|
+
);
|
|
71
74
|
} catch (error) {
|
|
72
75
|
logInternalError("register.autoRepair.turnHook", error);
|
|
73
76
|
}
|
|
@@ -456,17 +456,23 @@ async function runDeferredSessionCleanup(
|
|
|
456
456
|
|
|
457
457
|
// Reconcile stale runs found on disk
|
|
458
458
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
459
|
+
// RR-021 WI-1.5: reconcile is async (mapConcurrent bound 4) — keep this
|
|
460
|
+
// session_start callback synchronous; notify when the result lands.
|
|
461
|
+
void reconcileAllStaleRuns(extensionCtx.cwd, ctx.getManifestCache(extensionCtx.cwd), Date.now(), currentSessionId)
|
|
462
|
+
.then((staleResults) => {
|
|
463
|
+
if ((staleResults ?? []).length > 0) {
|
|
464
|
+
ctx.notifyOperator({
|
|
465
|
+
id: "stale_reconcile",
|
|
466
|
+
severity: "info",
|
|
467
|
+
source: "crash-recovery",
|
|
468
|
+
title: `Reconciled ${staleResults.length} stale run(s)`,
|
|
469
|
+
body: `Found and repaired ghost runs from previous sessions: ${staleResults.map((r) => r.runId).join(", ")}`,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
.catch((error) => {
|
|
474
|
+
logInternalError("register.sessionStart.reconcileStale", error);
|
|
468
475
|
});
|
|
469
|
-
}
|
|
470
476
|
} catch (error) {
|
|
471
477
|
logInternalError("register.sessionStart.reconcileStale", error);
|
|
472
478
|
}
|
|
@@ -87,6 +87,7 @@ export function startLifecycleWatchers(ctx: ExtensionContext, state: LifecycleSt
|
|
|
87
87
|
// LAZY: async-notifier pulls in debounce + cron helpers — defer
|
|
88
88
|
// until the first lifecycle install (deferred import within module).
|
|
89
89
|
void import("../async-notifier.ts").then(({ startAsyncRunNotifier }) => {
|
|
90
|
+
// LAZY: defer async-notifier until the lifecycle event fires
|
|
90
91
|
if (deps.isCleanedUp()) return;
|
|
91
92
|
startAsyncRunNotifier(ctx, deps.notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, {
|
|
92
93
|
generation: deps.ownerGeneration,
|
|
@@ -110,6 +111,7 @@ export function stopLifecycleWatchers(state: LifecycleState, deps: LifecycleDeps
|
|
|
110
111
|
state.notifierStarted = false;
|
|
111
112
|
try {
|
|
112
113
|
void import("../async-notifier.ts").then(({ stopAsyncRunNotifier }) => {
|
|
114
|
+
// LAZY: defer async-notifier until the lifecycle event fires
|
|
113
115
|
stopAsyncRunNotifier(deps.notifierState);
|
|
114
116
|
});
|
|
115
117
|
} catch (error) {
|
|
@@ -71,7 +71,11 @@ export interface ObservabilityDeps {
|
|
|
71
71
|
* newer session. Reuse this counter — do not invent a parallel one.
|
|
72
72
|
*/
|
|
73
73
|
getSessionGeneration: () => number;
|
|
74
|
-
reconcileStaleRuns: (
|
|
74
|
+
reconcileStaleRuns: (
|
|
75
|
+
cwd: string,
|
|
76
|
+
cache: ReturnType<ObservabilityDeps["getManifestCache"]>,
|
|
77
|
+
currentSessionId?: string,
|
|
78
|
+
) => unknown[] | Promise<unknown[] | undefined>;
|
|
75
79
|
reconcileOrphanedTempWorkspaces: (now: number, opts: { cleanupOrphanedTempDirs?: boolean }) => unknown;
|
|
76
80
|
cleanupOrphanTempDirs: () => { cleaned: number; scanned: number; failed: number };
|
|
77
81
|
cleanupLegacyOrphanTempDirs: () => { cleaned: number; scanned: number; failed: number };
|
|
@@ -253,26 +257,31 @@ async function configureObservabilityInternal(ctx: ExtensionContext, state: Obse
|
|
|
253
257
|
if (autoRepairIntervalMs > 0) {
|
|
254
258
|
state.autoRepairTimer = setInterval(() => {
|
|
255
259
|
if (deps.isCleanedUp()) return;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
260
|
+
// RR-021 WI-1.5: reconcileStaleRuns may be async (mapConcurrent) —
|
|
261
|
+
// settle the result before inspecting, keep errors inside the log.
|
|
262
|
+
void (async () => {
|
|
263
|
+
try {
|
|
264
|
+
const settled = await deps.reconcileStaleRuns(ctx.cwd, deps.getManifestCache(ctx.cwd), extractSessionId(ctx));
|
|
265
|
+
const staleResults = Array.isArray(settled) ? settled : [];
|
|
266
|
+
if (staleResults.length > 0) {
|
|
267
|
+
for (const result of staleResults) {
|
|
268
|
+
const repaired = (result as { repaired?: boolean }).repaired;
|
|
269
|
+
if (repaired) {
|
|
270
|
+
deps.notifyOperator({
|
|
271
|
+
id: `auto_repair_${(result as { runId: string }).runId}`,
|
|
272
|
+
severity: "info",
|
|
273
|
+
source: "auto-repair",
|
|
274
|
+
runId: (result as { runId: string }).runId,
|
|
275
|
+
title: `Auto-repaired stale run`,
|
|
276
|
+
body: (result as { detail?: string }).detail ?? "",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
270
279
|
}
|
|
271
280
|
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logInternalError("register.autoRepair", error);
|
|
272
283
|
}
|
|
273
|
-
}
|
|
274
|
-
logInternalError("register.autoRepair", error);
|
|
275
|
-
}
|
|
284
|
+
})();
|
|
276
285
|
}, autoRepairIntervalMs);
|
|
277
286
|
state.autoRepairTimer.unref();
|
|
278
287
|
|
|
@@ -2,7 +2,7 @@ import { statSync } from "node:fs";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { TObject } from "@sinclair/typebox";
|
|
5
|
-
import {
|
|
5
|
+
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
6
6
|
import { loadConfig } from "../../config/config.ts";
|
|
7
7
|
import { findClosestKey } from "../../config/suggestions.ts";
|
|
8
8
|
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
@@ -36,6 +36,12 @@ import { withSessionId } from "../team-tool/context.ts";
|
|
|
36
36
|
import { formatTeamToolParamError } from "../team-tool/param-error.ts";
|
|
37
37
|
import { toolResult } from "../tool-result.ts";
|
|
38
38
|
|
|
39
|
+
// RR-021 WI-4.3g: compile the team-tool schema ONCE. Value.Check re-walks the
|
|
40
|
+
// whole (large) schema object on every tool call; TypeCompiler.Compile (STATIC
|
|
41
|
+
// method on @sinclair/typebox/compiler) builds a specialized checker at module
|
|
42
|
+
// load. The error formatter keeps using the schema + Value.Errors (cold path).
|
|
43
|
+
const compiledTeamToolParams = TypeCompiler.Compile(TeamToolParams);
|
|
44
|
+
|
|
39
45
|
/**
|
|
40
46
|
* Normalize model-generated tool params before schema validation.
|
|
41
47
|
*
|
|
@@ -228,7 +234,8 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
228
234
|
// validation so strict unions/patterns accept "unset" as omitted.
|
|
229
235
|
const normalized = normalizeTeamParams(params);
|
|
230
236
|
// Defense-in-depth: validate params at runtime even though Pi framework already does
|
|
231
|
-
|
|
237
|
+
// (RR-021 WI-4.3g: precompiled checker — same semantics as Value.Check.)
|
|
238
|
+
if (!compiledTeamToolParams.Check(normalized)) {
|
|
232
239
|
return toolResult(formatTeamToolParamError(TeamToolParams, normalized), { action: "list", status: "error" }, true);
|
|
233
240
|
}
|
|
234
241
|
// EXT-1: additionalProperties:true lets unknown (typo'd) keys slip past
|
|
@@ -28,6 +28,7 @@ export function installCrossExtensionWiring(pi: ExtensionAPI, ctx: RegistrationC
|
|
|
28
28
|
// in the entire runtime chain) out of the cold-start module graph. EXT-9:
|
|
29
29
|
// the registry now lives in module-scoped state, not globalThis.
|
|
30
30
|
void import("../team-tool.ts").then(({ installCrewGlobalRegistry }) => {
|
|
31
|
+
// LAZY: defer team-tool until cross-extension wiring fires
|
|
31
32
|
const manifestCacheForRegistry = ctx.getManifestCache(ctx.currentCtx?.cwd ?? process.cwd());
|
|
32
33
|
installCrewGlobalRegistry({
|
|
33
34
|
manifestCache: manifestCacheForRegistry,
|
|
@@ -57,6 +58,7 @@ export function refreshCrossExtensionWiringForSession(pi: ExtensionAPI, ctx: Reg
|
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
void import("../team-tool.ts").then(({ installCrewGlobalRegistry }) => {
|
|
61
|
+
// LAZY: defer team-tool until cross-extension wiring fires
|
|
60
62
|
installCrewGlobalRegistry({
|
|
61
63
|
manifestCache: ctx.getManifestCache(ctx.currentCtx?.cwd ?? process.cwd()),
|
|
62
64
|
cwdProvider: () => ctx.currentCtx?.cwd ?? process.cwd(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { listRecentRuns } from "./run-index.ts";
|
|
3
3
|
// Lazy-loaded: team-tool.ts pulls in entire runtime chain.
|
|
4
4
|
import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
|
|
5
5
|
|
|
@@ -122,7 +122,8 @@ export async function handleTeamManagerCommand(_args: string, ctx: ExtensionComm
|
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// RR-021 WI-4.1: bounded recent-runs read instead of a full index scan.
|
|
126
|
+
const runs = listRecentRuns(ctx.cwd, 20);
|
|
126
127
|
if (runs.length === 0) {
|
|
127
128
|
ctx.ui.notify("No pi-crew runs found.", "info");
|
|
128
129
|
return;
|
|
@@ -21,6 +21,7 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
21
21
|
import type { GoalWrapWorkflowConfig } from "../../config/types.ts";
|
|
22
22
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
23
23
|
import { GoalStore } from "../../runtime/goal-workflow/goal-state-store.ts";
|
|
24
|
+
import { captureRunModelContext } from "../../runtime/model/session-model.ts";
|
|
24
25
|
import { snapshotManifests } from "../../runtime/verification/verification-integrity.ts";
|
|
25
26
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
26
27
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
@@ -248,6 +249,11 @@ export async function startGoalWrappedRun(
|
|
|
248
249
|
store.save(goalState);
|
|
249
250
|
|
|
250
251
|
const paths = createRunPaths(cwd, goalId);
|
|
252
|
+
// Finding #1 (battery 2026-09-29): persist the parent session's model
|
|
253
|
+
// routing on the goal-loop manifest — the DETACHED runner re-loads this
|
|
254
|
+
// manifest and has no ExtensionContext to re-resolve it. Without it,
|
|
255
|
+
// goal-wrap turns silently default-chain (minimax 402 family).
|
|
256
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
251
257
|
const goalLoopManifest: TeamRunManifest = {
|
|
252
258
|
schemaVersion: 1,
|
|
253
259
|
runId: goalId,
|
|
@@ -267,6 +273,7 @@ export async function startGoalWrappedRun(
|
|
|
267
273
|
artifacts: [],
|
|
268
274
|
ownerSessionId,
|
|
269
275
|
runKind: "goal-loop",
|
|
276
|
+
...(modelContext ? { modelContext } : {}),
|
|
270
277
|
};
|
|
271
278
|
await saveRunManifestAsync(goalLoopManifest);
|
|
272
279
|
await appendEventAsync(paths.eventsPath, {
|