pi-crew 0.9.11 → 0.9.13

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 (35) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -135
  3. package/package.json +1 -1
  4. package/src/extension/crew-shortcuts.ts +29 -2
  5. package/src/extension/knowledge-injection.ts +236 -15
  6. package/src/extension/registration/commands.ts +61 -34
  7. package/src/extension/team-tool/chain-dispatch.ts +103 -0
  8. package/src/extension/team-tool/chain-executor.ts +400 -0
  9. package/src/extension/team-tool/run.ts +9 -0
  10. package/src/runtime/agent-memory.ts +5 -2
  11. package/src/runtime/background-runner.ts +49 -0
  12. package/src/runtime/chain-runner.ts +47 -8
  13. package/src/runtime/child-pi.ts +33 -0
  14. package/src/runtime/handoff-manager.ts +7 -0
  15. package/src/runtime/live-session-runtime.ts +10 -4
  16. package/src/runtime/pi-json-output.ts +5 -1
  17. package/src/runtime/process-status.ts +7 -2
  18. package/src/runtime/task-output-context.ts +36 -7
  19. package/src/runtime/task-runner/prompt-builder.ts +5 -1
  20. package/src/runtime/task-runner.ts +7 -0
  21. package/src/schema/team-tool-schema.ts +9 -0
  22. package/src/ui/crew-footer.ts +3 -3
  23. package/src/ui/crew-select-list.ts +1 -1
  24. package/src/ui/dashboard-panes/agents-pane.ts +26 -4
  25. package/src/ui/dashboard-panes/cancellation-pane.ts +23 -0
  26. package/src/ui/keybinding-map.ts +7 -3
  27. package/src/ui/live-conversation-overlay.ts +2 -2
  28. package/src/ui/live-run-sidebar.ts +18 -10
  29. package/src/ui/overlays/help-overlay.ts +166 -0
  30. package/src/ui/run-dashboard.ts +210 -70
  31. package/src/ui/status-colors.ts +45 -0
  32. package/src/ui/widget/index.ts +46 -3
  33. package/src/ui/widget/widget-formatters.ts +22 -7
  34. package/src/ui/widget/widget-renderer.ts +31 -27
  35. package/src/utils/visual.ts +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.9.13] — chain feature + raw worker output + anti-zombie hardening (2026-06-29)
4
+
5
+ Context/compaction efficiency work + a live `chain` feature that wires previously-dead code, plus two root-cause zombie fixes. The unifying theme: **deliver the right context efficiently and never leave orphaned state behind.**
6
+
7
+ ### Features
8
+
9
+ - **Section-aware knowledge injection** (`knowledge-injection.ts`) — `.crew/knowledge.md` is no longer injected as a uniform head-only block. CONVENTIONS sections (always-relevant: Code Style, Environment, Architecture, Testing, Release Process) are injected in full; SESSION-LOG sections (per-version post-mortems, incidents) are IDF-scored against the task/goal and budget-capped (5 KB), drop-whole with a head-slice fallback and a section-index recovery net. Cuts knowledge-token waste for trivial tasks (a `team action='run'` with a trivial goal went from ~4 000 tokens of mostly-irrelevant session-log noise to ~2 095 tokens of conventions + matching session-log) without starving architecture-relevant tasks. Backward compatible: `readKnowledge(cwd)` with no query keeps the legacy head-only path. Verified live (post-restart research run): worker knowledge dropped from ~4 000 tokens (95 % session-log noise) to ~2 095 tokens, with the synthesized Architecture reference present.
10
+
11
+ - **Live `chain` feature** (`team action='run', chain='"a" -> "b" -> "c"'`) — `ChainRunner` was complete and unit-tested but had ZERO production callers. Each step now runs a REAL team run via an injected `handleRun`, with handoff context (including the previous step's output text) passed forward through a `# Previous Steps in This Chain` block prepended to the step's goal. This also makes `enrichContextFromHandoffs`'s honesty markers (`__chainHistoryNotes`) reachable in production. See `src/extension/team-tool/{chain-dispatch,chain-executor}.ts`.
12
+
13
+ ### Bug fixes
14
+
15
+ - **Raw worker result + dependency-context tee recovery (output)** — `results/<id>.txt` was 16 K-capped at child-pi stream-parse time (`compactContentPart` caps assistant text at `MAX_ASSISTANT_TEXT_CHARS`), and the dependency-context path re-read the same capped text circularly (no tee, unlike sharedReads). Now: (a) `ChildPiLineObserver` captures the RAW final assistant text before the transcript's compaction, and `task-runner` writes it as the authoritative `results/<id>.txt` (monotonic-safe fallback to transcript-derived `finalText`); the transcript stays 16 K-capped (telemetry memory bound unchanged). (b) `collectDependencyOutputContext` now uses `readIfSmallWithTee` (matching sharedReads) and `renderDependencyOutputContext` surfaces the `Full output (if you need the missing middle): <path>` hint, breaking the circular re-read. Verified live: a research run's analyst result went from a 16 K-capped file with a `[pi-crew compacted N chars]` marker to a 33 KB RAW file with zero data-loss markers; the downstream writer received 33 K of raw dependency context. Tracing in `research-findings/output-handling-deep-dive.md`.
16
+
17
+ - **Truncated-content recovery hints** — truncation markers in knowledge (`knowledge-injection.ts`), agent memory (`agent-memory.ts`), and dependency output now embed the absolute path + a `read`-tool hint so workers can recover the dropped tail instead of silently losing it. Tee threshold lowered 2× → 1.25× (`TEE_THRESHOLD_MULTIPLIER`) so the 32–64 KB band gets a recovery path. Live-session system prompt no longer duplicates the MEMORY block (it was already injected via `renderTaskPrompt().full`).
18
+
19
+ - **background-runner watchdog (anti-zombie processes)** — the keepAlive interval (`setInterval(()=>{}, 5000)`, no `unref`) cleared only in `runCleanup`, so a hung team run (stuck child Pi, deadlocked lock, or a test that spawns a run without cleanup) left the process alive indefinitely. The existing parent-guard only fires when the parent DIES; here the parent hung too, so it never fired. Fix: a watchdog timer alongside keepAlive aborts via the shared `AbortController` (propagates → child-pi → kills child processes) and force-exits after a 15 s grace if the abort does not propagate. Default 2 h (generous for legitimate long runs); override via `PI_CREW_MAX_RUN_MS`. Verified live: normal run exits cleanly (watchdog cleared, 0 `watchdog_fired` events); isolated hang fires at exactly `MAX` ms.
20
+
21
+ - **Test state isolation (anti-zombie UI rows)** — `useProjectState(cwd)` returns `findRepoRoot(cwd) !== undefined`, and `scopeBaseRoot` falls back to the EXTENSION-GLOBAL state dir (`~/.pi/agent/extensions/pi-crew/state/runs/`) when cwd is NOT a git repo. Tests that create a tmpdir WITHOUT `git init` and call `createRunManifest`/`writeRunFixture`/`createRunPaths` were leaking run records into that global dir — the one the crew widget reads — creating persistent fake-agent rows after every test run. Fix: the shared `createTrackedTempDir` helper and `state-store.test.ts`'s `makeResolvedTempDir` now create a `.git` marker dir so records land in `<tmpdir>/.crew/` (auto-cleaned). Protects all current and future callers. Verified: state-store + cov + chain-executor tests left global state 2 → 2 (zero leak).
22
+
23
+ ### Docs
24
+
25
+ - **README** — removed 182 lines of version-by-version highlights (v0.9.0 → v0.9.12, v0.8.x, v0.7.0) that duplicated CHANGELOG.md. README now flows intro → `## Features` straight. Version tags remain only where they qualify a feature (e.g. `(L4, v0.9.8)`) — useful for deciding whether to upgrade.
26
+
27
+ ### Stats
28
+
29
+ 29 files changed · ~2 498 insertions / ~189 deletions across 7 commits.
30
+
31
+ ### Verification
32
+
33
+ - `npx tsc --noEmit`: clean.
34
+ - 44/44 new + modified unit tests pass (`raw-final-text`, `dependency-tee`, `chain-executor`, `chain-handoff-markers`, `background-runner-watchdog`).
35
+ - Live (post-restart): chain 2-step run produced enriched step-2 goal with step 1's output; raw worker result verified end-to-end (33 KB analyst result, 0 markers); watchdog verified on normal path (clean exit) + hang path (fires at MAX ms).
36
+
37
+ ## [v0.9.12] — TUI UI/UX polish (21 findings) (2026-06-27)
38
+
39
+ Comprehensive TUI UX review of pi-crew's UI layer (`src/ui/` + `src/utils/visual.ts`). 21 findings (5×P1, 10×P2, 6×P3), all addressed. Full review with evidence-backed file:line citations: `research-findings/pi-crew-uiux-review.md`.
40
+
41
+ ### Bug fixes
42
+
43
+ - **F-3 (P1)** — `src/ui/live-conversation-overlay.ts`: local `pad` used `s.length` (counted ANSI escape bytes) and `content.slice` (UTF-16 units) → border drift on every colored or CJK line. Replaced with `pad`/`truncate` from `utils/visual.ts` (reference impl: `transcript-viewer.ts`).
44
+ - **F-1 / F-2 / V-3 (P1/P2)** — `src/ui/status-colors.ts` (new shared `colorizeStatusGlyphs()`): unified status-glyph colorization covers ⏳ (waiting), ⚠ (needs_attention), and the braille spinner range ⠁–⣿ — the two most attention-demanding states were previously uncolored. Replaces duplicated per-module glyph maps/regexes in `widget-renderer.ts`, `live-run-sidebar.ts`, and `run-dashboard.ts`.
45
+ - **L-1 (P1)** — `src/ui/run-dashboard.ts`: windowed run list with `scrollOffset`; selection can no longer escape the rendered 8-row window. ↓ past row 7 previously hid the highlight and Enter acted on an invisible run. Brute-force verified for 0–30 runs.
46
+ - **L-2 (P1)** — cancellation/failure reason now shown in default detail row (`run-dashboard.ts`) and `live-run-sidebar.ts`. Previously rendered only in `progress-pane` (pane `2`). `cancellation-pane.ts` wired in via `summarizeTerminalReason()` (D-1).
47
+ - **V-1 (P2)** — tabular-aligned numeric metrics across `run-dashboard` footer, `dashboard-panes/agents-pane.ts`, and `widget/widget-formatters.ts` via width-aware `alignMetric` (visibleWidth-based). Eliminates per-tick column jitter on transitions like 9.9s→10.0s and 950→1.0k.
48
+ - **F-5 (P2)** — `src/runtime/process-status.ts`: `ERROR_VISIBILITY_GRACE_MS = 10 * 60_000` (was the 8s `COMPLETED_VISIBILITY_GRACE_MS` shared with completed runs). Failed/cancelled runs now linger 10 min in the crew widget — the run-level header (✗ team/workflow · X/Y agents) is the "one-line trace". Successful completions still vanish in 8s.
49
+ - **K-1 (P2)** — new `src/ui/overlays/help-overlay.ts`: `?` opens the HelpOverlay rendering `BINDINGS[]` grouped by scope. ~16/20 keybindings were previously undiscoverable (no `?` cheatsheet existed). Header hint updated.
50
+ - **F-6 (P2)** — `src/ui/live-run-sidebar.ts`: auto-close countdown moved inside the bordered box (was rendered below the bottom border).
51
+ - **V-2 (P2)** — `src/ui/crew-footer.ts`, `src/ui/crew-select-list.ts`: dropped ASCII `'...'` 3rd arg from `truncate(...)` so they use the default `'…'` (U+2026) consistently with the rest of the UI. Test updated.
52
+ - **L-3 / L-4 / L-5 (P2/P3)** — width-aware `runLabel` keeps the goal visible (the most meaningful field); widget prioritizes running > queued > waiting with `finishedSlots` so finished rows only fill leftover budget and never push a live agent's activity line off-screen; run-list separator unified to ` · `.
53
+ - **F-4 / F-7 (P2/P3)** — stale-snapshot hint in the default dashboard view when manifest reads are flaky; actionable empty/error states (no more "Dashboard error — see logs").
54
+ - **T-1 (P3)** — `src/utils/visual.ts`: ZWJ (`U+200D`) removed from `WIDE_RANGES` (now correctly width-0; was inflating compound-emoji width and over-truncating).
55
+ - **T-2 (P3)** — `src/ui/widget/index.ts`: debounced (~120ms) SIGWINCH + stdout-resize listener busts the render cache and requests a repaint. Guarded against double-registration across widget reinstalls.
56
+ - **D-1 (P3)** — `src/ui/dashboard-panes/cancellation-pane.ts` wired in via `summarizeTerminalReason()` (was dead-imported by `test/unit/cancellation-pane.test.ts`, so kept and given a real consumer instead of deleted).
57
+
58
+ ### Shortcut collision fix
59
+
60
+ - **Extension-load warning**: `alt+d` collided with `tui.editor.deleteWordForward` (verified in pi-tui `TUI_KEYBINDINGS`). Pi resolved in favor of the extension, *stripping* the editor's delete-word-forward binding. Moved dashboard shortcut to **`alt+c`** (mnemonic: **C**rew — verified free against the full built-in `alt+` keymap: occupied letters are `b, d, f, v, y`).
61
+ - **Test guard hardened**: `test/unit/crew-shortcuts.test.ts` collision set now includes the editor keys (`alt+b/d/f/y`, `alt+backspace`, `alt+delete`). The previous set was incomplete (`alt+v, alt+enter, alt+arrows` only), which is why the bug slipped past tests. This class of regression is now caught at test time.
62
+
63
+ ### Decisions (deviations from the initial review)
64
+
65
+ - **K-3 `KEY_RESERVED`**: NOT dead code — consumed by `test/unit/keybinding-map.parity.test.ts:29,185-203` and `test/manual/l2-keybinding-dispatch-smoke.mjs`. Corrected the misleading "dead code" doc instead of deleting (deleting would have broken the parity test).
66
+ - **K-2 `alt+m` / `alt+t`**: blocked — mailbox overlay is run-scoped (requires a runId; reached via the dashboard); `team-status` is a text command (`handleTeamTool({action:"status"})`), not an overlay opener. `alt+d → dashboard` is the only wired shortcut.
67
+ - **F-5 location**: fixed at the correct layer (`process-status.ts` run-level grace) rather than the report's suggested `widget-renderer.ts` agent-row linger; the run-level header line is the right "one-line trace" per the finding.
68
+
69
+ ### Verification
70
+
71
+ - `npx tsc --noEmit` + `npm run typecheck`: 0 errors (incl. strip-types import smoke).
72
+ - Focused UI cluster: **74/74 pass** across 8 suites — `crew-shortcuts` (incl. strengthened collision assertion), `crew-footer` (incl. renamed V-2 test), `keybinding-map parity`, `cancellation-pane`, `process-status ×3`, `agents-pane-cost`.
73
+ - Full suite baseline (before this batch): 5642 / 5639 pass / 0 fail. Two post-change full runs hit `ETIMEDOUT` on `spawnSync` inside integration child-spawn tests (`worktree-run`, `cleanup-full-flow`) under load (0 assertion failures — environmental flake). To be reconfirmed against CI.
74
+
3
75
  ## [v0.9.11] — Per-run lock path for background-runner (parallel-spawn race) (2026-06-27)
4
76
 
5
77
  Bug caught by an E2E parallel-spawn test in this session, NOT by unit tests (which cannot spawn multiple real processes). Independent of the F1-F5/redaction batches.
package/README.md CHANGED
@@ -39,141 +39,7 @@ npm: pi-crew
39
39
  repo: https://github.com/baphuongna/pi-crew
40
40
  ```
41
41
 
42
- **v0.9.4 / v0.9.5 / v0.9.8 / v0.9.9**: See [CHANGELOG.md](CHANGELOG.md).
43
-
44
- ### Highlights (v0.6.4 → v0.9.9)
45
-
46
- A long arc of **trust, cliff-resilience, and robustness** work. Principle: *build
47
- trust and cliff-resilience, stay lean, delete before adding.*
48
-
49
- #### v0.9.5 — fix "team run hangs forever at 25%" (2026-06-23)
50
- Two coupled runtime bugs caused recurring "run stuck at 25% (1/4)" failures
51
- across 4+ consecutive review/fast-fix runs. The combined symptom: scheduler
52
- appears to stop responding right after the first task (explorer) finishes, no
53
- progress to task 2, and `team action='status'` returns "Run not found" with
54
- **no diagnostic trail** to investigate. Manual `kill` of the parent `pi`
55
- process was the only workaround.
56
-
57
- - **🩹 Bug X (proximate cause)** — `purgeStaleActiveRunIndex`
58
- (`src/runtime/crash-recovery.ts`) destroyed a run's `stateRoot` based on a
59
- **frozen** `entry.updatedAt` (set once at registration, never refreshed).
60
- Any long-running legitimate async run (≥5 min) whose worker had exited
61
- lost its entire durable state. `saveRunTasks()` then silently no-op'd on
62
- the missing dir, and the workflow could never advance. Fix: corroborate
63
- liveness via the on-disk `manifest.updatedAt` AND the team-level
64
- `heartbeat.json`; keep `stateRoot` on cancel so runs stay queryable and
65
- resumable.
66
- - **🩹 Bug Y (root cause — why the scheduler died in the first place)** —
67
- `src/runtime/background-runner.ts` redirected only `console.log` /
68
- `console.error` to the log file. The first post-detach `console.debug`
69
- call from `team-runner.ts:242` (inside `mergeTaskUpdatesPreservingTerminal`
70
- → "Skipping stale merge") hit the disconnected stdout pipe → unhandled
71
- `EPIPE` → process exit. Prior investigators concluded (incorrectly) that
72
- the cause was a native crash, because diagnostic `[DIAG]` handlers never
73
- fired on the EPIPE. Fix: extend the console redirect to `console.debug` /
74
- `console.warn`, and wrap `fs.writeSync` in try-catch so any log-write
75
- failure can never crash the scheduler.
76
- - **🧪 Regression coverage** — 7 new tests: 3 in
77
- `test/unit/crash-recovery-purge-liveness.test.ts` (fresh-manifest-kept,
78
- orphan-cancelled-preserved, fresh-heartbeat-kept) + 4 in
79
- `test/unit/background-runner-console-redirect.test.ts` (drift-detector
80
- pattern that exercises undefined / valid / EBADF / post-toggle logFd).
81
- - **📖 See [CHANGELOG.md](CHANGELOG.md) for full details**, including
82
- why prior attempts to diagnose the hang kept destroying the only
83
- evidence (Bug X nuked the stateRoot before anyone could read the EPIPE
84
- crash in Bug Y).
85
-
86
- > **Recovering a stuck run from v0.9.4 or earlier:** the `stateRoot` for
87
- > those runs is already gone. Re-dispatch the workflow — new runs are
88
- > fully protected.
89
-
90
- #### v0.9.4 — macOS CI fixture (2026-06-23)
91
- - **🧪 BSD-vs-GNU grep fix** — benchmark test fixtures used
92
- `grep --help` (exits 0 on GNU/Linux, exits 2 on BSD/macOS). Switched
93
- the exit-0 fixture to `echo ok`; the not-in-allowlist fixture is now
94
- `ls`. CI matrix is now green on all 3 OSes.
95
- - **📌 Process note** — this release re-commits to: **tag/publish ONLY
96
- after the full OS matrix CI is green.** v0.9.3 was published mid-CI-run
97
- (the macOS job hadn't finished); the package itself was correct (the
98
- broken file is test-only and not shipped), but the repo CI went red.
99
- v0.9.4 restores green CI. v0.9.5 follows the same discipline.
100
-
101
- #### v0.9.0 — goal loops + dynamic workflows (2026-06-18)
102
- Two new features, both modeled on Claude Code, built on a shared `runKind`
103
- background-dispatch discriminator.
104
-
105
- - **🎯 Autonomous goal loops** — `team action='goal'` runs a self-directed
106
- multi-turn loop: a **worker** does a turn, a separate **LLM judge**
107
- (capability-locked, no tools) evaluates the transcript + verification against
108
- the objective, and on "not-achieved" the reason is fed into the next turn's
109
- prompt. Stops on `achieved` / `maxTurns` / budget / `BLOCKED:` / user `stop`.
110
- See [docs/goals.md](docs/goals.md).
111
- - **📜 Dynamic workflows (`.dwf.ts`)** — author orchestration as a TypeScript
112
- script (JS loops/branch/cross-review) instead of a static step list. Runs in
113
- the background, spawns subagents via `ctx.agent()`/`ctx.fanOut()`, holds
114
- intermediate results in JS variables, and only `ctx.setResult()` reaches the
115
- main context. `workflow-create`/`-delete` are ACE-gated (`confirm:true`,
116
- user-confirmed). See [docs/dynamic-workflows.md](docs/dynamic-workflows.md).
117
- - **🛡️ Goal-wrap** (RFC v0.5 vision) — apply the goal completion-guarantee to
118
- existing builtin workflows (`implementation`, `fast-fix`, `default`) via
119
- per-workflow `.crew/config.json` toggle. Single-step workflows goal-wrap
120
- end-to-end; multi-step workflows auto-downgrade to a normal team-run because
121
- they crash non-deterministically under the V8/libuv event-loop (see [Known
122
- limitations](#known-limitations)).
123
- - **🔐 Phase 1 integrity hardening** (P1a–P1g) — verification bookend snapshots,
124
- anti-oscillation (`stuck` non-terminal + resumable), budget enforcement
125
- (required or explicit opt-out), nonce-token feedback sanitization, secret
126
- redaction at artifact-write (O(n) fix), global worker cap + workspace lock
127
- (O_EXCL, startTime-safe). B2 confused-deputy (auto-detecting verification
128
- commands) refused — user must declare verification explicitly.
129
- - **🧪 Phase 1.5 fast-follow** — opt-in mitigation toggles for residual risks:
130
- `PI_CREW_VERIFICATION_SANITIZE_ENV=1` (strip provider secrets from the
131
- verification subprocess), `PI_CREW_VERIFICATION_WORKTREE=1` (run verification
132
- in a pristine git worktree at the T_snap commit SHA),
133
- `PI_CREW_BG_REPORT_ON_FATAL=1` (V8 diagnostic report on fatal).
134
- - **🐛 TDZ fix** (Phase 1.5 #4) — live `team action='run' workflow='<dynamic>'`
135
- was failing with a misleading "must export a default async function" error.
136
- Root cause was a Temporal Dead Zone race in `team-tool/run.ts` when loaded via
137
- the full Pi extension pipeline (`index.ts → … → run.ts`). Fixed by
138
- `let`→`var` on the latch + lazy dynamic imports at call sites.
139
-
140
- #### v0.8.x — hardening & reliability (2026-06-17)
141
- - **🛠️ Split-scope install fix (v0.8.11)** — `team` runs no longer crash with
142
- `Cannot find module '@earendil-works/pi-coding-agent'` when pi-crew and pi
143
- live in separate node_modules trees (the default for `pi install`). New
144
- `src/runtime/peer-dep.ts` resolves the ESM-only peer dep across 6 strategies.
145
- - **🔄 Model fallback on transient 5xx (v0.8.11)** — a hard-down provider
146
- (`500 api_error "unknown error"`) now triggers the configured fallback
147
- model instead of aborting the run. `isRetryableModelFailure` extended.
148
- - **🧊 Cold-start race eliminated (v0.8.6 → v0.8.10)** — under tsx, concurrent
149
- subagent spawns raced module instantiation (`existsSync` / `CREW_README` /
150
- `effectiveRunConfig` / `validateWorkflowForTeam`). Fixed graph-wide: warm at
151
- registration + gate at spawn boundaries + per-site latches. 6/6 repro clean.
152
- - **🔒 Cross-project leak fixed (v0.8.8)** — ambient status / compaction no
153
- longer bleed foreign-project runs into the current session. Cwd-scope
154
- barrier (`isInProjectScope`), version-independent.
155
- - **🩺 Doctor runtime-warmup status (v0.8.7)** — `team doctor` shows whether
156
- the module-graph warmup fired.
157
- - **🔍 Cold-verifier agent (v0.8.4)** — adversarial cross-check that re-derives
158
- claims WITHOUT trusting prior analysis, catching confirmation bias.
159
- - **⚡ Per-write validator (v0.8.5)** — zero-cost `JSON.parse` on every
160
- `write`/`edit`, appends a `🔴` blocker on malformed files.
161
- - **🎨 Terminal status (v0.8.3)** — tab title + Ghostty native progress bar.
162
- - **🧠 Skill confidence revived (v0.8.2)** — `adjustConfidence()` was dead
163
- code; the effectiveness system now actually learns.
164
- - **🔧 Tool-restriction unification (v0.8.0)** — single `resolveToolPolicy`
165
- across both spawn paths.
166
- - **🎯 F6/F1 interop granularity (v0.7.9)** — 7 skill roots, `.pi/agents/`
167
- tier, tool wildcards, `excludeExtensions` denylist.
168
-
169
- #### v0.7.0 — Phase 0 + Phase 1 roadmap
170
- - **🛡️ Compaction resilience (O10)** — in-flight runs survive auto-compact.
171
- - **💰 Cost visibility (O1)** — per-role token + cost attribution.
172
- - **✋ Plan-level HITL (O5)** — `requirePlanApproval` gates any workflow.
173
- - **🧠 Cross-run memory (O4)** — `.crew/knowledge.md` injected every run.
174
- - **🎯 Single-agent cliff hedge** — `team plan singleAgent=true`.
175
42
 
176
- ---
177
43
 
178
44
  ## Features
179
45
 
@@ -697,7 +563,7 @@ npm run ci # full CI-equivalent check
697
563
  npm pack --dry-run # package verification
698
564
  ```
699
565
 
700
- Stats: **366 source files** (70K lines) · **506 test files** (66K lines) · **4,792 tests, 0 failures** · **CI: Ubuntu ✅ macOS ✅ Windows ✅**
566
+ Stats: **431 source files** (87K lines) · **606 test files** (85K lines) · **~5,860 tests, 0 failures** · **CI: Ubuntu ✅ macOS ✅ Windows ✅**
701
567
 
702
568
  ---
703
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -6,9 +6,25 @@
6
6
  * built-in keymap (see analysis of pi-tui core/keybindings defaults):
7
7
  *
8
8
  * alt+s → open the pi-crew settings overlay (config + theme picker)
9
+ * alt+c → open the pi-crew run dashboard overlay (mnemonic: **C**rew)
9
10
  *
10
- * `alt+<letter>` combos are safe: Pi only binds `alt+v`, `alt+enter`, and the
11
- * alt+arrow navigation keys. `alt+s` is mnemonic (settings) and free.
11
+ * OCCUPIED alt+ keys in the built-in keymap (must NOT reuse verified against
12
+ * pi-tui TUI_KEYBINDINGS + pi core keybindings.js):
13
+ * alt+b (cursor word left) alt+f (cursor word right)
14
+ * alt+d (delete word forward) alt+y (yank pop)
15
+ * alt+v (paste) alt+s (crew settings — this module)
16
+ * alt+enter / alt+up/down/left/right / alt+backspace / alt+delete
17
+ * Free alt+<letter> keys include: a, c, e, g, h, i, j, k, l, m, n, o, p, q,
18
+ * r, t, u, w, x, z.
19
+ *
20
+ * NOTE: an earlier revision used `alt+d` for the dashboard; that collided
21
+ * with `tui.editor.deleteWordForward` and Pi's conflict detector stripped the
22
+ * editor binding. `alt+c` is free AND mnemonic.
23
+ *
24
+ * NOTE: alt+m (mailbox) and alt+t (status) were considered but are NOT wired
25
+ * — the mailbox overlay is run-scoped (requires a runId; reached via the
26
+ * dashboard) and there is no standalone status overlay (status is a text
27
+ * command). See the K-2 note accompanying openTeamDashboard in commands.ts.
12
28
  *
13
29
  * Shortcuts are guarded by `hasUI` so they never fire in print/RPC mode, and
14
30
  * by the optional `registerShortcut` API so older Pi versions degrade
@@ -39,6 +55,17 @@ const CREW_SHORTCUTS: ReadonlyArray<ShortcutRegistration> = [
39
55
  await openTeamSettingsOverlay(ctx);
40
56
  },
41
57
  },
58
+ {
59
+ key: "alt+c",
60
+ description: "pi-crew: open run dashboard (Crew)",
61
+ // Lazy-import so the heavy UI module chain (RunDashboard etc.) is only
62
+ // loaded on first use, not at extension load.
63
+ handler: async (ctx) => {
64
+ // LAZY: defer dynamic import of ./registration/commands.ts to its call site.
65
+ const { openTeamDashboard } = await import("./registration/commands.ts");
66
+ await openTeamDashboard(ctx);
67
+ },
68
+ },
42
69
  ];
43
70
 
44
71
  /**
@@ -9,6 +9,16 @@
9
9
  * Philosophy (Round 6 stress-test): radically downsized. Just a Markdown
10
10
  * file the user can edit, surfaced into every run. No vector DB, no
11
11
  * embeddings, no graph. Simple = trustworthy.
12
+ *
13
+ * B2 section-aware injection (2026-06-28): knowledge.md splits cleanly into
14
+ * CONVENTIONS (always-relevant: Code Style, Environment, Architecture,
15
+ * Testing, Release Process — ~2.5KB) and SESSION-LOG (per-version post-
16
+ * mortems, incidents, fix detail — ~29KB, rarely relevant). Conventions
17
+ * are ALWAYS injected; session-log sections are injected on-demand via
18
+ * header-token IDF scoring against the task/goal, capped at
19
+ * MAX_SESSION_LOG_BYTES. Non-matched session-log is summarized as a section
20
+ * index with a `read` path-hint so the worker can recover any omitted topic.
21
+ * Design doc: research-findings/b2-section-aware-design.md.
12
22
  */
13
23
  import * as fs from "node:fs";
14
24
  import * as path from "node:path";
@@ -17,20 +27,179 @@ import { projectCrewRoot } from "../utils/paths.ts";
17
27
 
18
28
  /** The knowledge file, relative to the project crew root. */
19
29
  export const KNOWLEDGE_FILENAME = "knowledge.md";
20
- /** Cap injected knowledge to avoid unbounded system-prompt growth. */
21
- const MAX_KNOWLEDGE_BYTES = 16_000;
30
+
31
+ /**
32
+ * Session-log injection cap (B2). Conventions are always injected in full;
33
+ * matched session-log sections are capped here to bound total prompt size.
34
+ * Sized so 2-3 typical sections (~1-2.5KB each) fit, while the largest single
35
+ * outlier (v0.9.10 IN PROGRESS, ~7KB) is excluded from a full-match scenario.
36
+ */
37
+ const MAX_SESSION_LOG_BYTES = 5_000;
38
+
39
+ /**
40
+ * Relevance context for section-aware injection. When omitted (or when goal/
41
+ * taskText are both absent), injection falls back to conventions-only — no
42
+ * IDF computation, no session-log body. This keeps callers without query
43
+ * context (main-session hook, legacy tests) stable.
44
+ */
45
+ export interface KnowledgeQuery {
46
+ /** The team run goal (broad topic signal). */
47
+ goal?: string;
48
+ /** The step/task instruction text (narrow topic signal). */
49
+ taskText?: string;
50
+ /** The worker role (reserved for future role-floor/boost; not scored yet). */
51
+ role?: string;
52
+ }
53
+
54
+ /**
55
+ * Headers of sections always treated as CONVENTIONS (universal project rules).
56
+ * Anything NOT matching these (after the convention run) is SESSION-LOG.
57
+ * Header-matching is prefix + case-insensitive against the H2 title.
58
+ */
59
+ const CONVENTION_HEADERS = ["Code Style", "Environment", "Architecture", "Testing", "Release Process"];
22
60
 
23
61
  /** Resolve the knowledge file path for a cwd (may not exist yet). */
24
62
  export function knowledgePath(cwd: string): string {
25
63
  return path.join(projectCrewRoot(cwd), KNOWLEDGE_FILENAME);
26
64
  }
27
65
 
28
- /** Read knowledge content, truncated to a safe size. "" if absent/empty. */
29
- export function readKnowledge(cwd: string): string {
66
+ interface KnowledgeSection {
67
+ /** H2 header text (without leading `## `). */
68
+ header: string;
69
+ /** Full section body including the `## ` header line. */
70
+ body: string;
71
+ /** Header tokens (lowercased, stopword-filtered) for IDF scoring. */
72
+ headerTokens: Set<string>;
73
+ }
74
+
75
+ const STOPWORDS = new Set([
76
+ "the", "a", "an", "is", "are", "of", "to", "in", "for", "and", "or", "not",
77
+ "with", "this", "that", "it", "be", "on", "at", "by", "do", "does", "how",
78
+ "what", "when", "from", "fix", "fixes", "fixed", "v0", "v1", "released",
79
+ "progress", "uncommitted", "not", "pushed", "published", "session", "post",
80
+ ]);
81
+
82
+ /** Tokenize header text into a Set of lowercased non-stopword tokens. */
83
+ function tokenizeHeader(header: string): Set<string> {
84
+ const tokens = new Set<string>();
85
+ for (const raw of header.toLowerCase().split(/[^a-z0-9.]+/)) {
86
+ // keep dot-paths (e.g. "run.lock", "config.ts") and alphanumerics >= 2 chars
87
+ const t = raw.replace(/^\.+|\.+$/g, "");
88
+ if (t.length >= 2 && !STOPWORDS.has(t) && !/^\d+$/.test(t)) tokens.add(t);
89
+ }
90
+ return tokens;
91
+ }
92
+
93
+ /** Parse knowledge.md into (conventions, sessionLog) sections by H2 header. */
94
+ function parseKnowledgeSections(content: string): { conventions: KnowledgeSection[]; sessionLog: KnowledgeSection[] } {
95
+ const lines = content.split(/\r?\n/);
96
+ const sections: KnowledgeSection[] = [];
97
+ let current: { header: string; lines: string[] } | null = null;
98
+ for (const line of lines) {
99
+ const m = /^##\s+(.+?)\s*$/.exec(line);
100
+ if (m) {
101
+ if (current) sections.push({ header: current.header, body: current.lines.join("\n"), headerTokens: tokenizeHeader(current.header) });
102
+ current = { header: m[1]!, lines: [line] };
103
+ } else if (current) {
104
+ current.lines.push(line);
105
+ }
106
+ }
107
+ if (current) sections.push({ header: current.header, body: current.lines.join("\n"), headerTokens: tokenizeHeader(current.header) });
108
+
109
+ // Classify: a section is a CONVENTION if its header starts-with one of the
110
+ // known convention titles. Once we leave the convention run (first non-
111
+ // matching H2 after conventions), everything else is SESSION-LOG. This is
112
+ // self-healing: if knowledge.md is restructured, the classifier adapts.
113
+ const conventions: KnowledgeSection[] = [];
114
+ const sessionLog: KnowledgeSection[] = [];
115
+ let leftConventionRun = false;
116
+ for (const sec of sections) {
117
+ const isConvention = !leftConventionRun && CONVENTION_HEADERS.some((c) => sec.header.toLowerCase().startsWith(c.toLowerCase()));
118
+ if (isConvention) conventions.push(sec);
119
+ else {
120
+ leftConventionRun = true;
121
+ sessionLog.push(sec);
122
+ }
123
+ }
124
+ return { conventions, sessionLog };
125
+ }
126
+
127
+ /** Compute IDF (inverse document frequency) over session-log header tokens. */
128
+ function computeIdf(sections: KnowledgeSection[]): Map<string, number> {
129
+ const N = sections.length;
130
+ const df = new Map<string, number>();
131
+ for (const s of sections) for (const token of s.headerTokens) df.set(token, (df.get(token) ?? 0) + 1);
132
+ const idf = new Map<string, number>();
133
+ for (const [token, freq] of df) idf.set(token, Math.log(N / freq)); // standard IDF; freq >= 1
134
+ return idf;
135
+ }
136
+
137
+ /** Tokenize a free-form query (goal/taskText) the same way as headers. */
138
+ function tokenizeQuery(query: string): Set<string> {
139
+ const tokens = new Set<string>();
140
+ for (const raw of query.toLowerCase().split(/[^a-z0-9.]+/)) {
141
+ const t = raw.replace(/^\.+|\.+$/g, "");
142
+ if (t.length >= 2 && !STOPWORDS.has(t) && !/^\d+$/.test(t)) tokens.add(t);
143
+ }
144
+ return tokens;
145
+ }
146
+
147
+ /** Score a section by summed IDF of query tokens present in its header. */
148
+ function scoreSection(queryTokens: Set<string>, headerTokens: Set<string>, idf: Map<string, number>): number {
149
+ let score = 0;
150
+ for (const token of queryTokens) if (headerTokens.has(token)) score += idf.get(token) ?? 0;
151
+ return score;
152
+ }
153
+
154
+ /**
155
+ * Select session-log sections relevant to the query, capped at budgetBytes.
156
+ * Greedy by descending score (then original order), drop-whole policy with a
157
+ * head-slice fallback for the single best match if nothing else fits (so a
158
+ * matched query never returns zero bytes).
159
+ */
160
+ function selectSessionLog(query: string, sessionLog: KnowledgeSection[], budgetBytes: number): KnowledgeSection[] {
161
+ if (sessionLog.length === 0 || !query.trim()) return [];
162
+ const idf = computeIdf(sessionLog);
163
+ const queryTokens = tokenizeQuery(query);
164
+ // Match = ANY header-token overlap with the query. IDF still drives RANKING
165
+ // (rarer overlap ranks first), but a token that happens to appear in every
166
+ // section (IDF=0) must still count as a match — otherwise a query for a
167
+ // common-but-relevant keyword (e.g. "redaction") would return nothing.
168
+ const scored = sessionLog
169
+ .map((sec, idx) => {
170
+ const overlap = [...queryTokens].filter((t) => sec.headerTokens.has(t));
171
+ return { sec, score: overlap.reduce((sum, t) => sum + (idf.get(t) ?? 0), 0), idx, hasOverlap: overlap.length > 0 };
172
+ })
173
+ .filter((x) => x.hasOverlap);
174
+ if (scored.length === 0) return [];
175
+ scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
176
+
177
+ const selected: KnowledgeSection[] = [];
178
+ let used = 0;
179
+ let bestMatch: { sec: KnowledgeSection; score: number } | undefined;
180
+ for (const { sec, score } of scored) {
181
+ if (score > (bestMatch?.score ?? -1)) bestMatch = { sec, score };
182
+ if (used + sec.body.length <= budgetBytes) {
183
+ selected.push(sec);
184
+ used += sec.body.length;
185
+ }
186
+ }
187
+ // Head-slice fallback: if the best match didn't fit (or nothing fit), inject
188
+ // a head-sliced copy of the single best match so the query isn't empty.
189
+ if (selected.length === 0 && bestMatch) {
190
+ const sliced = bestMatch.sec.body.slice(0, Math.max(0, budgetBytes));
191
+ selected.push({ ...bestMatch.sec, body: `${sliced}\n\n<!-- section truncated (session-log budget ${budgetBytes} bytes). Full file: use \`read\`. -->` });
192
+ }
193
+ return selected;
194
+ }
195
+
196
+ /** Read knowledge content. "" if absent/empty. */
197
+ export function readKnowledge(cwd: string, query?: KnowledgeQuery): string {
30
198
  try {
31
199
  const p = knowledgePath(cwd);
32
200
  const stat = tryStat(p);
33
201
  if (!stat) {
202
+ sectionCache.delete(p);
34
203
  knowledgeCache.delete(p);
35
204
  return "";
36
205
  }
@@ -39,14 +208,46 @@ export function readKnowledge(cwd: string): string {
39
208
  // For a run with N workers this is N redundant readFileSync of the same
40
209
  // file. Cache by (mtimeMs, size) and only re-read when the file changes.
41
210
  const cacheKey = `${stat.mtimeMs}:${stat.size}`;
42
- const cached = knowledgeCache.get(p);
43
- if (cached && cached.key === cacheKey) return cached.content;
44
- let content = fs.readFileSync(p, "utf8").trim();
45
- if (content.length > MAX_KNOWLEDGE_BYTES) {
46
- content = `${content.slice(0, MAX_KNOWLEDGE_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_BYTES} bytes -->`;
211
+
212
+ // B2: when no query context is provided (main-session hook, legacy
213
+ // callers, tests), keep the simple head-only path. This preserves the
214
+ // 4 existing unit tests exactly (they call readKnowledge(cwd)).
215
+ if (!query || (!query.goal && !query.taskText)) {
216
+ const cached = knowledgeCache.get(p);
217
+ if (cached && cached.key === cacheKey) return cached.content;
218
+ let content = fs.readFileSync(p, "utf8").trim();
219
+ if (content.length > MAX_KNOWLEDGE_HEAD_BYTES) {
220
+ content = `${content.slice(0, MAX_KNOWLEDGE_HEAD_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_HEAD_BYTES} bytes (head shown). Full file: ${p} — use the \`read\` tool if you need sections beyond the head. -->`;
221
+ }
222
+ knowledgeCache.set(p, { key: cacheKey, content });
223
+ return content;
224
+ }
225
+
226
+ // Section-aware path: cache parsed sections, select per-query.
227
+ const cachedSections = sectionCache.get(p);
228
+ let parsed: { conventions: KnowledgeSection[]; sessionLog: KnowledgeSection[] };
229
+ if (cachedSections && cachedSections.key === cacheKey) {
230
+ parsed = { conventions: cachedSections.conventions, sessionLog: cachedSections.sessionLog };
231
+ } else {
232
+ const content = fs.readFileSync(p, "utf8").trim();
233
+ parsed = parseKnowledgeSections(content);
234
+ sectionCache.set(p, { key: cacheKey, conventions: parsed.conventions, sessionLog: parsed.sessionLog });
235
+ }
236
+
237
+ const queryText = [query.goal, query.taskText].filter(Boolean).join(" \n ");
238
+ const matchedSessionLog = selectSessionLog(queryText, parsed.sessionLog, MAX_SESSION_LOG_BYTES);
239
+
240
+ const parts: string[] = [];
241
+ // Conventions: always full.
242
+ for (const sec of parsed.conventions) parts.push(sec.body);
243
+ // Matched session-log (drop-whole, budget-capped).
244
+ for (const sec of matchedSessionLog) parts.push(sec.body);
245
+ // Always: section-index of ALL session-log headers (recovery safety net).
246
+ if (parsed.sessionLog.length > 0) {
247
+ const indexLines = parsed.sessionLog.map((s) => ` - ${s.header}`);
248
+ parts.push(`<!-- Session-log sections in knowledge.md (not injected unless matched above — use \`read\` for detail):\n${indexLines.join("\n")}\nFull file: ${p} -->`);
47
249
  }
48
- knowledgeCache.set(p, { key: cacheKey, content });
49
- return content;
250
+ return parts.join("\n").trim();
50
251
  } catch {
51
252
  return "";
52
253
  }
@@ -68,9 +269,19 @@ interface CachedKnowledge {
68
269
  }
69
270
  const knowledgeCache = new Map<string, CachedKnowledge>();
70
271
 
272
+ interface CachedSections {
273
+ key: string;
274
+ conventions: KnowledgeSection[];
275
+ sessionLog: KnowledgeSection[];
276
+ }
277
+ const sectionCache = new Map<string, CachedSections>();
278
+
279
+ /** Head cap for the no-query (legacy / main-session) path. */
280
+ const MAX_KNOWLEDGE_HEAD_BYTES = 2_000;
281
+
71
282
  /** Build the injected prompt fragment (empty if no knowledge). */
72
- export function buildKnowledgeFragment(cwd: string): string {
73
- const content = readKnowledge(cwd);
283
+ export function buildKnowledgeFragment(cwd: string, query?: KnowledgeQuery): string {
284
+ const content = readKnowledge(cwd, query);
74
285
  if (!content) return "";
75
286
  return [
76
287
  "",
@@ -85,8 +296,18 @@ export function buildKnowledgeFragment(cwd: string): string {
85
296
 
86
297
  /**
87
298
  * Register the knowledge-injection hook. Appends project knowledge to the
88
- * system prompt on every agent start (main session + each crew worker,
89
- * since workers are child Pi processes that also fire before_agent_start).
299
+ * MAIN session's system prompt on `before_agent_start`. This hook does NOT
300
+ * fire for crew workers: they are spawned with `--no-extensions`, so the
301
+ * extension layer (and this hook) never loads in their process. Workers
302
+ * instead receive knowledge via `buildKnowledgeFragment(task.cwd)` injected
303
+ * into their prompt stablePrefix by `prompt-builder.ts`. Do NOT "fix" this
304
+ * perceived gap by making the hook reach workers — it would cause
305
+ * double-injection. (Verified by research workflow 2026-06-28.)
306
+ *
307
+ * The hook calls buildKnowledgeFragment(cwd) with NO query — so the main
308
+ * session gets conventions-only (no session-log noise), which is the right
309
+ * default for an interactive session that doesn't have a single task focus.
310
+ * Workers (which DO have a task) use the section-aware path via prompt-builder.
90
311
  */
91
312
  export function registerKnowledgeInjection(pi: ExtensionAPI): void {
92
313
  pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {