pi-crew 0.9.10 → 0.9.12
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 +90 -0
- package/package.json +1 -1
- package/src/config/role-tools.ts +39 -6
- package/src/extension/crew-shortcuts.ts +29 -2
- package/src/extension/registration/commands.ts +61 -34
- package/src/runtime/async-runner.ts +70 -74
- package/src/runtime/background-runner.ts +13 -2
- package/src/runtime/process-status.ts +7 -2
- package/src/runtime/role-permission.ts +5 -21
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/state/artifact-store.ts +22 -2
- package/src/ui/crew-footer.ts +3 -3
- package/src/ui/crew-select-list.ts +1 -1
- package/src/ui/dashboard-panes/agents-pane.ts +26 -4
- package/src/ui/dashboard-panes/cancellation-pane.ts +23 -0
- package/src/ui/keybinding-map.ts +7 -3
- package/src/ui/live-conversation-overlay.ts +2 -2
- package/src/ui/live-run-sidebar.ts +18 -10
- package/src/ui/overlays/help-overlay.ts +166 -0
- package/src/ui/run-dashboard.ts +210 -70
- package/src/ui/status-colors.ts +45 -0
- package/src/ui/widget/index.ts +46 -3
- package/src/ui/widget/widget-formatters.ts +22 -7
- package/src/ui/widget/widget-renderer.ts +31 -27
- package/src/utils/redaction.ts +49 -31
- package/src/utils/visual.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.9.12] — TUI UI/UX polish (21 findings) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
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`.
|
|
6
|
+
|
|
7
|
+
### Bug fixes
|
|
8
|
+
|
|
9
|
+
- **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`).
|
|
10
|
+
- **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`.
|
|
11
|
+
- **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.
|
|
12
|
+
- **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).
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
- **F-6 (P2)** — `src/ui/live-run-sidebar.ts`: auto-close countdown moved inside the bordered box (was rendered below the bottom border).
|
|
17
|
+
- **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.
|
|
18
|
+
- **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 ` · `.
|
|
19
|
+
- **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").
|
|
20
|
+
- **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).
|
|
21
|
+
- **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.
|
|
22
|
+
- **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).
|
|
23
|
+
|
|
24
|
+
### Shortcut collision fix
|
|
25
|
+
|
|
26
|
+
- **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`).
|
|
27
|
+
- **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.
|
|
28
|
+
|
|
29
|
+
### Decisions (deviations from the initial review)
|
|
30
|
+
|
|
31
|
+
- **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).
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
### Verification
|
|
36
|
+
|
|
37
|
+
- `npx tsc --noEmit` + `npm run typecheck`: 0 errors (incl. strip-types import smoke).
|
|
38
|
+
- 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`.
|
|
39
|
+
- 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.
|
|
40
|
+
|
|
41
|
+
## [v0.9.11] — Per-run lock path for background-runner (parallel-spawn race) (2026-06-27)
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
### Bug fix
|
|
46
|
+
|
|
47
|
+
- **Shared `run.lock` killed concurrent background runners** (`src/runtime/background-runner.ts:417`). The bootstrap call passed a fake manifest `{ stateRoot: "", runId, cwd }` to `withRunLockSync` because the real manifest was not loaded yet. `lockPath()` = `path.join(manifest.stateRoot, "run.lock")` = `path.join("", "run.lock")` = `"run.lock"` — a RELATIVE path at cwd, SHARED across every run regardless of runId. When multiple background agents spawned in the same instant (e.g. parallel `Agent` calls), they raced on the single shared lock: one acquired, the rest failed fast ("Run 'run.lock' is locked by another operation") and exited within 3s. The existing "FIX Issue #3" comment claimed to prevent concurrent runners "for the same runId", but the lock path never contained the runId. Fix: compute the real per-run stateRoot via `createRunPaths(cwd, runId).stateRoot` before locking, so each run locks its own `<cwd>/.crew/state/runs/<runId>/run.lock`. Matches `locks-race.test.ts`.
|
|
48
|
+
|
|
49
|
+
### Verification
|
|
50
|
+
|
|
51
|
+
- `npx tsc --noEmit` EXIT 0
|
|
52
|
+
- 7 lock-related suites pass (locks-race 10, background-runner-console-redirect 4, async-runner 13, api-locks 1, orphan-worker-registry 15, locks-untested 11, team-runner-heartbeat 2)
|
|
53
|
+
- E2E reproduce (decisive): BEFORE the fix, 3 parallel background explorers → 1 pass + 2 fail (background.log: "Failed to acquire lock"). AFTER the fix, same scenario → 3/3 pass, 0 lock errors.
|
|
54
|
+
|
|
55
|
+
### Lesson
|
|
56
|
+
|
|
57
|
+
Concurrency/lock bugs only reproduce when multiple real processes spawn simultaneously — unit tests mocking a single process can never catch them. E2E parallel-spawn smoke tests are the only way to verify. (Reinforces the v0.9.9 lesson: E2E with real extension load is decisive.)
|
|
58
|
+
|
|
59
|
+
## [v0.9.11] — Read-only permission model fixes F1-F5 (2026-06-27)
|
|
60
|
+
|
|
61
|
+
Review of the role permission model (question: "do read-only workflows still persist their task output?") confirmed output persistence is runner-driven and correct, but found 5 findings — one the same defect class as the v0.9.10 writer incident (Fix 5), in the opposite direction.
|
|
62
|
+
|
|
63
|
+
### Bug fixes
|
|
64
|
+
|
|
65
|
+
- **`security-reviewer`/`test-engineer` tool config unreachable (F1, HIGH)** (`src/config/role-tools.ts`). Map keys were `security_reviewer`/`test_engineer` (underscore) while the runtime role strings are hyphenated (`agents/security-reviewer.md` → `security-reviewer`). `getToolConfig` did not normalize, so it returned `{}` and the strictest tool restrictions in the codebase silently never applied. Same defect class as the writer incident, opposite direction (under-enforce vs over-enforce). Tests masked it: they queried only the underscore forms. Fix: quote+hyphen the keys (a bare `security-reviewer:` key parses as subtraction — must be quoted) and normalize in `getToolConfig` (`role.replaceAll("_","-")`); added a regression test that derives role names from the runtime sets and asserts each resolves its intended config.
|
|
66
|
+
- **`critic`/`planner` tool-config gaps (F2)** (`src/config/role-tools.ts`). `critic` had no entry (a custom critic agent had no tool-level read-only enforcement); `planner`'s entry only excluded `ask_question` and did not enforce read-only. Added a `critic` entry and strengthened `planner` to a read-only tool-set.
|
|
67
|
+
- **`planner` kept read-only with deliverable guidance (F3)** (`src/runtime/task-runner/prompt-builder.ts`). `planner` emits deliverables (`output: plan.md`) but moving it to WRITE_ROLES would fire the plan-approval gate BEFORE planning (breaking default/implementation workflows — `team-runner.ts:399` relies on planner being read-only). Fix: keep planner read-only and add a prompt line telling read-only roles their RESULT TEXT is persisted by the runner, so they emit deliverables as text instead of attempting file writes.
|
|
68
|
+
- **`verifier` reclassified read-only → write (F4)** (`src/runtime/role-permission.ts` + `src/config/role-tools.ts`). `verifier`'s task runs tests via bash with redirects/cache writes (`npm test | tee`, `mkdir`, `rm`), all forbidden by the read-only prompt gate — a direct contradiction with `agents/verifier.md`. Moved verifier to WRITE_ROLES; tool-config keeps bash but excludes edit/write so source integrity is preserved. Mirrors `cold-verifier`.
|
|
69
|
+
- **Dead command-enforcement removed (F5)** (`src/runtime/role-permission.ts`). `isReadOnlyCommand`/`checkRolePermission`/`READ_ONLY_COMMANDS` had zero runtime callers (only tests). Real protection lives in the role tool-config + `safe-paths.ts`/`resolveRealContainedPath` (10+ runtime callers). Deleted the dead code.
|
|
70
|
+
|
|
71
|
+
### Verification
|
|
72
|
+
|
|
73
|
+
- `npx tsc --noEmit` EXIT 0
|
|
74
|
+
- 124 tests pass / 0 fail across 13 suites + 1 integration (role-tools 15, role-permission-cov 23, role-permission 2, role-permission.spawn 3, prompt-builder-cov 15, v0-8-0-tool-policy-unification 10, skill-instructions 16, plan-approval-boundary 7, crew-contracts 6, goal-loop-team-roles 5, t9-cold-verifier 5, completion-guard 7, verification-gates 10, role-tools-integration 3)
|
|
75
|
+
- E2E: `research` workflow 3/3 tasks — explorer+analyst (read-only) persisted findings, writer wrote the deliverable file
|
|
76
|
+
|
|
77
|
+
## [v0.9.11] — Secret redaction & env hardening (2026-06-27)
|
|
78
|
+
|
|
79
|
+
Independent security review (review team, 3/4 tasks, ~360K tokens) flagged 3 Medium findings in the secret-redaction and env-passthrough surfaces. All verified by live `npx tsx` repro + source trace before fixing.
|
|
80
|
+
|
|
81
|
+
### Bug fixes
|
|
82
|
+
|
|
83
|
+
- **`redactAuthHeader` leaked credential values (L3/L5)** (`src/utils/redaction.ts`). Two defects: (1) `indexOf` matched only the FIRST `authorization:` occurrence per call, so a second header on a later line leaked verbatim; (2) the word-boundary allow-list excluded `-` and `\t`, so `Proxy-Authorization:` / `X-Authorization:` and tab-indented headers were not recognized. Fix: loop over all occurrences and add `-`/`\t` to a shared `AUTH_HEADER_BOUNDARY_CHARS` set (used by both `redactAuthHeader` and `redactBearerTokens`). Latent weakness caught by repro (NOT by the reviewer's proposed fix): the old code only APPENDED a ` ***` marker without removing the value — `"authorization: Basic abc123"` became `"authorization: Basic abc123 ***"` (credential still visible). The redact branch now blanks the value: `line.substring(authIdx, authIdx+14) + " ***"` → `"authorization: ***"`. Consistent with `redactInlineSecrets`.
|
|
84
|
+
- **`writeArtifact` flat-redaction only (M2)** (`src/state/artifact-store.ts:130`). Applied only `redactSecretString` (flat regex scan), so quoted-JSON secrets (`"api_key":"sk-..."`) and nested keys survived into persisted artifacts (e.g. `startup-evidence.json` holds up to 500 chars of raw child stderr). Fix: structural-then-flat — when content parses as JSON, run `redactSecrets` (recursive) first, then flat `redactSecretString`. Order matters: structural catches quoted keys, flat still catches Bearer/JWT/Auth headers inside JSON string values. Formatting is preserved: the input is re-stringified with the SAME indentation (pretty → indent 2, compact → compact), so pretty-printed artifacts like group-join metadata keep their `"partial": false` whitespace (caught by `test/integration/phase4-runtime.test.ts` regression on CI after the first attempt shipped a compact re-stringify).
|
|
85
|
+
- **Provider API keys leaked into the detached background runner (M1)** (`src/runtime/async-runner.ts:162`). The env allowlist forwarded 14 provider keys (MINIMAX/OPENAI/ANTHROPIC/...) to the background runner, contradicting `child-pi.ts:275` ("API keys are NOT needed — config file"). Keys leaked into V8 fatal-error reports (`--report-on-fatalerror` writes `environmentVariables` unredacted). The inline comment "same as child-pi.ts" was false. Fix: extracted `BACKGROUND_RUNNER_ENV_ALLOWLIST` (exported, unit-testable) and removed the 14 provider keys. Prereq verified: `background-runner.ts` does not read provider keys directly.
|
|
86
|
+
|
|
87
|
+
### Verification
|
|
88
|
+
|
|
89
|
+
- `npx tsc --noEmit` EXIT 0
|
|
90
|
+
- 21 targeted suites pass (~130 tests): redaction-cov (32), redaction-p1f (18), redaction-transcript-roundtrip (3), child-pi-sec1-redaction (8), artifact-store (4), async-runner (13), env-filter (4), env-filter-cov (9), security-hardening (8), round28-otlp-crlf (4), child-pi-compaction-real (9), + others
|
|
91
|
+
- Live repro: `redactSecretString("Proxy-Authorization: Basic c2VjcmV0")` → `"Proxy-Authorization: ***"` (was: unchanged leak)
|
|
92
|
+
|
|
3
93
|
## [v0.9.10 (continued)] — Round 29 follow-ups: BG2 sweep bug fixes, test optimization, E2E verification (2026-06-26)
|
|
4
94
|
|
|
5
95
|
A full-suite verify run (`verify-full2`, 5502 tests, 774 suites) surfaced 4 file-level timeouts and 2 real correctness bugs. This release fixes the 2 real bugs, the underlying cause of 2 of the 4 timeouts (chain-runner + orphan-worker-registry + cleanup-full-flow self-deadlock + HandoffManager interval leak), and adds E2E verification artifacts to prove all fixes hold against the live runtime, not just static analysis.
|
package/package.json
CHANGED
package/src/config/role-tools.ts
CHANGED
|
@@ -22,9 +22,23 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
22
22
|
excludeTools: ["edit", "write", "ask_question"],
|
|
23
23
|
},
|
|
24
24
|
|
|
25
|
-
// Planner -
|
|
25
|
+
// Planner - Read-only planning; emits plans as TEXT (runner persists result).
|
|
26
|
+
// F2/F3: strengthened to a read-only tool-set matching its READ_ONLY_ROLES
|
|
27
|
+
// classification. Deliverables are emitted as RESULT TEXT (consumed by
|
|
28
|
+
// adaptive-plan.ts / runner shared-output), NOT file writes — so the
|
|
29
|
+
// plan-approval gate boundary (planner = read-only) is preserved. Moving
|
|
30
|
+
// planner to WRITE_ROLES would fire the gate before planning, breaking the
|
|
31
|
+
// default/implementation workflows.
|
|
26
32
|
planner: {
|
|
27
|
-
|
|
33
|
+
tools: ["read", "grep", "find", "ls", "glob"],
|
|
34
|
+
excludeTools: ["edit", "write", "bash", "web", "ask_question"],
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Critic - Read-only plan/design critique (F2: was missing from the map,
|
|
38
|
+
// so a custom critic agent had no tool-level read-only enforcement).
|
|
39
|
+
critic: {
|
|
40
|
+
tools: ["read", "grep", "find", "ls", "glob"],
|
|
41
|
+
excludeTools: ["edit", "write", "bash", "web"],
|
|
28
42
|
},
|
|
29
43
|
|
|
30
44
|
// Executor - Full access (default)
|
|
@@ -45,13 +59,26 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
45
59
|
},
|
|
46
60
|
|
|
47
61
|
// Security Reviewer - Strict restrictions
|
|
48
|
-
|
|
62
|
+
// F1: key is hyphenated to match the runtime role string (agents/
|
|
63
|
+
// security-reviewer.md → "security-reviewer"). The underscore form never
|
|
64
|
+
// resolved at runtime (returned {}), silently dropping enforcement.
|
|
65
|
+
"security-reviewer": {
|
|
49
66
|
tools: ["read", "grep", "find"],
|
|
50
67
|
excludeTools: ["edit", "write", "bash", "web", "ask_question"],
|
|
51
68
|
},
|
|
52
69
|
|
|
53
|
-
//
|
|
54
|
-
|
|
70
|
+
// Verifier - Runs tests (needs bash) but must NOT edit source (F4: moved
|
|
71
|
+
// from READ_ONLY_ROLES to WRITE_ROLES — the read-only prompt gate forbids
|
|
72
|
+
// the test-running redirects / cache writes its task requires, contradicting
|
|
73
|
+
// agents/verifier.md). Tool-set keeps bash but excludes edit/write so source
|
|
74
|
+
// integrity is preserved during verification. Mirrors cold-verifier behavior.
|
|
75
|
+
verifier: {
|
|
76
|
+
tools: ["read", "grep", "find", "ls", "bash"],
|
|
77
|
+
excludeTools: ["edit", "write", "web"],
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Test Engineer - Can write tests (F1: hyphenated key)
|
|
81
|
+
"test-engineer": {
|
|
55
82
|
tools: ["read", "edit", "write", "bash", "ls"],
|
|
56
83
|
excludeTools: ["web"],
|
|
57
84
|
},
|
|
@@ -61,7 +88,13 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
61
88
|
* Get tool configuration for a specific role.
|
|
62
89
|
*/
|
|
63
90
|
export function getToolConfig(role: string): RoleToolConfig {
|
|
64
|
-
|
|
91
|
+
// F1: normalize hyphen/underscore. Runtime role strings are hyphenated
|
|
92
|
+
// (agents/security-reviewer.md → "security-reviewer") but map keys were
|
|
93
|
+
// historically underscored, silently returning {} at runtime — the same
|
|
94
|
+
// defect class as the v0.9.10 writer incident (opposite direction:
|
|
95
|
+
// under-enforce instead of over-enforce). Accept both forms.
|
|
96
|
+
const key = role.includes("_") ? role.replaceAll("_", "-") : role;
|
|
97
|
+
return ROLE_TOOL_CONFIGS[key] ?? ROLE_TOOL_CONFIGS[role] ?? {};
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
/**
|
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
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
|
/**
|
|
@@ -260,6 +260,66 @@ async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selecti
|
|
|
260
260
|
|
|
261
261
|
let depsRef: RegisterTeamCommandsDeps | undefined;
|
|
262
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Open the pi-crew run dashboard overlay and run its action loop.
|
|
265
|
+
*
|
|
266
|
+
* Extracted verbatim from the `team-dashboard` command so it is reusable from
|
|
267
|
+
* a keyboard shortcut (alt+c, see crew-shortcuts.ts). Takes the base
|
|
268
|
+
* `ExtensionContext` (the shortcut handler's context) — uses only `hasUI`,
|
|
269
|
+
* `cwd`, `ui`, and `sessionManager` fields, so both `ExtensionContext` and
|
|
270
|
+
* `ExtensionCommandContext` satisfy it. Reads run caches via the module-level
|
|
271
|
+
* `depsRef` (set by `registerTeamCommands`), so it is a no-op if commands
|
|
272
|
+
* have not been registered yet. `deps` is captured into a local const so the
|
|
273
|
+
* non-undefined narrowing survives the awaited overlay call.
|
|
274
|
+
*
|
|
275
|
+
* The dashboard action helpers (handleMailboxDashboardAction, the viewers,
|
|
276
|
+
* notifyCommandResult, teamCommandContext/handleTeamTool) are declared for
|
|
277
|
+
* `ExtensionCommandContext` but only ever read base `ExtensionContext` fields
|
|
278
|
+
* (verified). The keyboard-shortcut path supplies an `ExtensionContext`, so we
|
|
279
|
+
* bridge those over-typed helpers with a single cast rather than relaxing
|
|
280
|
+
* signatures across several modules (some outside this file's scope).
|
|
281
|
+
*/
|
|
282
|
+
export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
283
|
+
if (!ctx.hasUI) return;
|
|
284
|
+
const deps = depsRef;
|
|
285
|
+
if (!deps) return;
|
|
286
|
+
const cmdCtx = ctx as ExtensionCommandContext;
|
|
287
|
+
for (;;) {
|
|
288
|
+
// Extract sessionId for workspace-scoped filtering
|
|
289
|
+
const sessionId = cmdCtx.sessionManager?.getSessionId?.();
|
|
290
|
+
const runs = deps.getManifestCache(cmdCtx.cwd).list(50);
|
|
291
|
+
const uiConfig = loadConfig(cmdCtx.cwd).config.ui;
|
|
292
|
+
const rightPanel = (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) === "right";
|
|
293
|
+
const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth)) : "90%";
|
|
294
|
+
const { RunDashboard } = await ui();
|
|
295
|
+
const selection = await cmdCtx.ui.custom<RunDashboardSelection | undefined>((tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(cmdCtx.cwd), runProvider: () => deps.getManifestCache(cmdCtx.cwd).list(50), registry: deps.getMetricRegistry?.(), workspaceId: sessionId, requestRender: () => requestRenderTarget(tui) }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
|
|
296
|
+
if (!selection) return;
|
|
297
|
+
if (selection.action === "reload") continue;
|
|
298
|
+
if (selection.action === "notifications-dismiss") {
|
|
299
|
+
deps.dismissNotifications?.();
|
|
300
|
+
cmdCtx.ui.notify("pi-crew notifications dismissed.", "info");
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (selection.action === "mailbox-detail") {
|
|
304
|
+
await handleMailboxDashboardAction(cmdCtx, selection.runId);
|
|
305
|
+
deps.getRunSnapshotCache?.(cmdCtx.cwd).invalidate(selection.runId);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (selection.action === "health-recovery" || selection.action === "health-kill-stale" || selection.action === "health-diagnostic-export") {
|
|
309
|
+
await handleHealthDashboardAction(cmdCtx, selection);
|
|
310
|
+
deps.getRunSnapshotCache?.(cmdCtx.cwd).invalidate(selection.runId);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (selection.action === "agent-transcript" && await openTranscriptViewer(cmdCtx, selection.runId)) continue;
|
|
314
|
+
if (selection.action === "agent-live" && await openLiveConversation(cmdCtx, selection.runId)) continue;
|
|
315
|
+
if (selection.action === "agent-live") { await notifyCommandResult(cmdCtx, commandText({ content: [{ type: "text", text: "No live agent found for this run." }] })); continue; }
|
|
316
|
+
const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, teamCommandContext(cmdCtx)) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, teamCommandContext(cmdCtx)) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, teamCommandContext(cmdCtx)) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, teamCommandContext(cmdCtx)) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, teamCommandContext(cmdCtx)) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, teamCommandContext(cmdCtx)) : // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
317
|
+
await handleTeamTool({ action: selection.action as any, runId: selection.runId }, teamCommandContext(cmdCtx));
|
|
318
|
+
await notifyCommandResult(cmdCtx, commandText(result));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
263
323
|
export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
|
|
264
324
|
depsRef = deps;
|
|
265
325
|
pi.registerCommand("teams", {
|
|
@@ -497,40 +557,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
497
557
|
} });
|
|
498
558
|
|
|
499
559
|
pi.registerCommand("team-dashboard", { description: "Open a pi-crew run dashboard overlay", handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
500
|
-
|
|
501
|
-
// Extract sessionId for workspace-scoped filtering
|
|
502
|
-
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
503
|
-
const runs = deps.getManifestCache(ctx.cwd).list(50);
|
|
504
|
-
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
505
|
-
const rightPanel = (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) === "right";
|
|
506
|
-
const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth)) : "90%";
|
|
507
|
-
const { RunDashboard } = await ui();
|
|
508
|
-
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50), registry: deps.getMetricRegistry?.(), workspaceId: sessionId, requestRender: () => requestRenderTarget(tui) }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
|
|
509
|
-
if (!selection) return;
|
|
510
|
-
if (selection.action === "reload") continue;
|
|
511
|
-
if (selection.action === "notifications-dismiss") {
|
|
512
|
-
deps.dismissNotifications?.();
|
|
513
|
-
ctx.ui.notify("pi-crew notifications dismissed.", "info");
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
if (selection.action === "mailbox-detail") {
|
|
517
|
-
await handleMailboxDashboardAction(ctx, selection.runId);
|
|
518
|
-
deps.getRunSnapshotCache?.(ctx.cwd).invalidate(selection.runId);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (selection.action === "health-recovery" || selection.action === "health-kill-stale" || selection.action === "health-diagnostic-export") {
|
|
522
|
-
await handleHealthDashboardAction(ctx, selection);
|
|
523
|
-
deps.getRunSnapshotCache?.(ctx.cwd).invalidate(selection.runId);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
|
|
527
|
-
if (selection.action === "agent-live" && await openLiveConversation(ctx, selection.runId)) continue;
|
|
528
|
-
if (selection.action === "agent-live") { await notifyCommandResult(ctx, commandText({ content: [{ type: "text", text: "No live agent found for this run." }] })); continue; }
|
|
529
|
-
const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, teamCommandContext(ctx)) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, teamCommandContext(ctx)) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, teamCommandContext(ctx)) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, teamCommandContext(ctx)) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, teamCommandContext(ctx)) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, teamCommandContext(ctx)) : // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
530
|
-
await handleTeamTool({ action: selection.action as any, runId: selection.runId }, teamCommandContext(ctx));
|
|
531
|
-
await notifyCommandResult(ctx, commandText(result));
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
560
|
+
await openTeamDashboard(ctx);
|
|
534
561
|
} });
|
|
535
562
|
|
|
536
563
|
pi.registerCommand("team-mascot", { description: "Show an animated mascot splash", handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -150,6 +150,75 @@ export interface SpawnBackgroundTeamRunResult {
|
|
|
150
150
|
logPath: string;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Env vars explicitly forwarded to the detached background runner.
|
|
155
|
+
*
|
|
156
|
+
* Provider API keys (MINIMAX/OPENAI/ANTHROPIC/...) are INTENTIONALLY OMITTED
|
|
157
|
+
* (security review M1): the background runner only spawns child Pi workers,
|
|
158
|
+
* which read keys from the Pi config file (not env). Passing keys via env
|
|
159
|
+
* leaks them into V8 fatal-error reports (--report-on-fatalerror writes the
|
|
160
|
+
* `environmentVariables` section unredacted). Matches child-pi.ts policy.
|
|
161
|
+
* Exported so the invariant is unit-testable (test/unit/async-runner.test.ts).
|
|
162
|
+
*/
|
|
163
|
+
export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
|
|
164
|
+
// Essential non-secret vars
|
|
165
|
+
"PATH",
|
|
166
|
+
"HOME",
|
|
167
|
+
"USER",
|
|
168
|
+
"SHELL",
|
|
169
|
+
"TERM",
|
|
170
|
+
"LANG",
|
|
171
|
+
"LC_ALL",
|
|
172
|
+
"LC_COLLATE",
|
|
173
|
+
"LC_CTYPE",
|
|
174
|
+
"LC_MESSAGES",
|
|
175
|
+
"LC_MONETARY",
|
|
176
|
+
"LC_NUMERIC",
|
|
177
|
+
"LC_TIME",
|
|
178
|
+
"XDG_CONFIG_HOME",
|
|
179
|
+
"XDG_DATA_HOME",
|
|
180
|
+
"XDG_CACHE_HOME",
|
|
181
|
+
"XDG_RUNTIME_DIR",
|
|
182
|
+
// Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
|
|
183
|
+
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
184
|
+
"NVM_BIN",
|
|
185
|
+
"NVM_DIR",
|
|
186
|
+
"NVM_INC",
|
|
187
|
+
"NODE_PATH",
|
|
188
|
+
"NODE_DISABLE_COLORS",
|
|
189
|
+
"NODE_EXTRA_CA_CERTS",
|
|
190
|
+
"NPM_CONFIG_REGISTRY",
|
|
191
|
+
"NPM_CONFIG_USERCONFIG",
|
|
192
|
+
"NPM_CONFIG_GLOBALCONFIG",
|
|
193
|
+
// PI_CREW_PARENT_PID is needed for parent-guard (liveness check).
|
|
194
|
+
"PI_CREW_DEPTH",
|
|
195
|
+
"PI_CREW_MAX_DEPTH",
|
|
196
|
+
"PI_CREW_INHERIT_PROJECT_CONTEXT",
|
|
197
|
+
"PI_CREW_INHERIT_SKILLS",
|
|
198
|
+
"PI_CREW_PARENT_PID",
|
|
199
|
+
"PI_TEAMS_DEPTH",
|
|
200
|
+
"PI_TEAMS_MAX_DEPTH",
|
|
201
|
+
"PI_TEAMS_INHERIT_PROJECT_CONTEXT",
|
|
202
|
+
"PI_TEAMS_INHERIT_SKILLS",
|
|
203
|
+
"PI_TEAMS_PI_BIN",
|
|
204
|
+
"PI_TEAMS_MOCK_CHILD_PI",
|
|
205
|
+
"PI_CREW_ALLOW_MOCK",
|
|
206
|
+
// Phase 1.5: worker-thread atomic writer opt-in (RFC 15).
|
|
207
|
+
"PI_CREW_WORKER_ATOMIC_WRITER",
|
|
208
|
+
"PI_TEAMS_WORKER_ATOMIC_WRITER",
|
|
209
|
+
// Phase 1.5 #1: verification env sanitization opt-in (RFC 13 §6).
|
|
210
|
+
"PI_CREW_VERIFICATION_SANITIZE_ENV",
|
|
211
|
+
"PI_TEAMS_VERIFICATION_SANITIZE_ENV",
|
|
212
|
+
"PI_CREW_VERIFICATION_PRESERVE_ENV",
|
|
213
|
+
"PI_TEAMS_VERIFICATION_PRESERVE_ENV",
|
|
214
|
+
// Phase 1.5 #2: verification git-worktree sandbox opt-in (RFC 16).
|
|
215
|
+
"PI_CREW_VERIFICATION_WORKTREE",
|
|
216
|
+
"PI_TEAMS_VERIFICATION_WORKTREE",
|
|
217
|
+
// Phase 1.5 #3: V8 diagnostic report on fatal error (RFC 17 — investigation).
|
|
218
|
+
"PI_CREW_BG_REPORT_ON_FATAL",
|
|
219
|
+
"PI_TEAMS_BG_REPORT_ON_FATAL",
|
|
220
|
+
];
|
|
221
|
+
|
|
153
222
|
export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise<SpawnBackgroundTeamRunResult> {
|
|
154
223
|
const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "background-runner.ts");
|
|
155
224
|
const logPath = path.join(manifest.stateRoot, "background.log");
|
|
@@ -159,80 +228,7 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
159
228
|
// to prevent leaking all env vars (including secrets) to detached background runner.
|
|
160
229
|
// Previously, destructuring only removed PI_CREW_PARENT_PID but kept everything else.
|
|
161
230
|
const filteredEnv = sanitizeEnvSecrets(process.env, {
|
|
162
|
-
allowList:
|
|
163
|
-
// Model provider API keys (same as child-pi.ts)
|
|
164
|
-
"MINIMAX_API_KEY",
|
|
165
|
-
"MINIMAX_GROUP_ID",
|
|
166
|
-
"OPENAI_API_KEY",
|
|
167
|
-
"OPENAI_ORG_ID",
|
|
168
|
-
"ANTHROPIC_API_KEY",
|
|
169
|
-
"GOOGLE_API_KEY",
|
|
170
|
-
"GOOGLE_GENERATIVE_LANGUAGE_API_KEY",
|
|
171
|
-
"AZURE_OPENAI_API_KEY",
|
|
172
|
-
"AZURE_OPENAI_ENDPOINT",
|
|
173
|
-
"AWS_ACCESS_KEY_ID",
|
|
174
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
175
|
-
"AWS_REGION",
|
|
176
|
-
"ZEU_API_KEY",
|
|
177
|
-
"ZERODEV_API_KEY",
|
|
178
|
-
// Essential non-secret vars
|
|
179
|
-
"PATH",
|
|
180
|
-
"HOME",
|
|
181
|
-
"USER",
|
|
182
|
-
"SHELL",
|
|
183
|
-
"TERM",
|
|
184
|
-
"LANG",
|
|
185
|
-
"LC_ALL",
|
|
186
|
-
"LC_COLLATE",
|
|
187
|
-
"LC_CTYPE",
|
|
188
|
-
"LC_MESSAGES",
|
|
189
|
-
"LC_MONETARY",
|
|
190
|
-
"LC_NUMERIC",
|
|
191
|
-
"LC_TIME",
|
|
192
|
-
"XDG_CONFIG_HOME",
|
|
193
|
-
"XDG_DATA_HOME",
|
|
194
|
-
"XDG_CACHE_HOME",
|
|
195
|
-
"XDG_RUNTIME_DIR",
|
|
196
|
-
// Windows essentials — see WINDOWS_ESSENTIAL_ENV_VARS (src/utils/env-allowlist.ts).
|
|
197
|
-
...WINDOWS_ESSENTIAL_ENV_VARS,
|
|
198
|
-
"NVM_BIN",
|
|
199
|
-
"NVM_DIR",
|
|
200
|
-
"NVM_INC",
|
|
201
|
-
"NODE_PATH",
|
|
202
|
-
"NODE_DISABLE_COLORS",
|
|
203
|
-
"NODE_EXTRA_CA_CERTS",
|
|
204
|
-
"NPM_CONFIG_REGISTRY",
|
|
205
|
-
"NPM_CONFIG_USERCONFIG",
|
|
206
|
-
"NPM_CONFIG_GLOBALCONFIG",
|
|
207
|
-
// FIX: explicit list matches child-pi.ts to prevent regression.
|
|
208
|
-
// PI_CREW_PARENT_PID is needed for parent-guard (liveness check).
|
|
209
|
-
"PI_CREW_DEPTH",
|
|
210
|
-
"PI_CREW_MAX_DEPTH",
|
|
211
|
-
"PI_CREW_INHERIT_PROJECT_CONTEXT",
|
|
212
|
-
"PI_CREW_INHERIT_SKILLS",
|
|
213
|
-
"PI_CREW_PARENT_PID",
|
|
214
|
-
"PI_TEAMS_DEPTH",
|
|
215
|
-
"PI_TEAMS_MAX_DEPTH",
|
|
216
|
-
"PI_TEAMS_INHERIT_PROJECT_CONTEXT",
|
|
217
|
-
"PI_TEAMS_INHERIT_SKILLS",
|
|
218
|
-
"PI_TEAMS_PI_BIN",
|
|
219
|
-
"PI_TEAMS_MOCK_CHILD_PI",
|
|
220
|
-
"PI_CREW_ALLOW_MOCK",
|
|
221
|
-
// Phase 1.5: worker-thread atomic writer opt-in (RFC 15).
|
|
222
|
-
"PI_CREW_WORKER_ATOMIC_WRITER",
|
|
223
|
-
"PI_TEAMS_WORKER_ATOMIC_WRITER",
|
|
224
|
-
// Phase 1.5 #1: verification env sanitization opt-in (RFC 13 §6).
|
|
225
|
-
"PI_CREW_VERIFICATION_SANITIZE_ENV",
|
|
226
|
-
"PI_TEAMS_VERIFICATION_SANITIZE_ENV",
|
|
227
|
-
"PI_CREW_VERIFICATION_PRESERVE_ENV",
|
|
228
|
-
"PI_TEAMS_VERIFICATION_PRESERVE_ENV",
|
|
229
|
-
// Phase 1.5 #2: verification git-worktree sandbox opt-in (RFC 16).
|
|
230
|
-
"PI_CREW_VERIFICATION_WORKTREE",
|
|
231
|
-
"PI_TEAMS_VERIFICATION_WORKTREE",
|
|
232
|
-
// Phase 1.5 #3: V8 diagnostic report on fatal error (RFC 17 — investigation).
|
|
233
|
-
"PI_CREW_BG_REPORT_ON_FATAL",
|
|
234
|
-
"PI_TEAMS_BG_REPORT_ON_FATAL",
|
|
235
|
-
],
|
|
231
|
+
allowList: BACKGROUND_RUNNER_ENV_ALLOWLIST,
|
|
236
232
|
});
|
|
237
233
|
// FIX: removed delete workarounds — with explicit allowlist, these vars
|
|
238
234
|
// are no longer auto-leaked. Matches child-pi.ts.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
withRunLockSync,
|
|
8
8
|
} from "../state/locks.ts";
|
|
9
9
|
import {
|
|
10
|
+
createRunPaths,
|
|
10
11
|
loadRunManifestById,
|
|
11
12
|
saveRunManifest,
|
|
12
13
|
updateRunStatus,
|
|
@@ -411,11 +412,21 @@ async function main(): Promise<void> {
|
|
|
411
412
|
);
|
|
412
413
|
// FIX Issue #3: Wrap in withRunLockSync to prevent concurrent background-runners
|
|
413
414
|
// for the same runId from reading stale manifest state. If lock cannot be
|
|
414
|
-
// acquired within 5s, fail immediately rather than proceeding with stale data.
|
|
415
|
+
// be acquired within 5s, fail immediately rather than proceeding with stale data.
|
|
416
|
+
//
|
|
417
|
+
// BUGFIX (caught by E2E parallel-spawn, 2026-06-27): the lock manifest must
|
|
418
|
+
// carry the REAL per-run stateRoot, NOT an empty string. lockPath() derives
|
|
419
|
+
// `<stateRoot>/run.lock`, so `stateRoot: ""` collapses every concurrent
|
|
420
|
+
// background-runner (different runIds, same spawn instant) onto a SINGLE
|
|
421
|
+
// shared `run.lock` at cwd — 1 acquires, the rest fail-fast and die. Compute
|
|
422
|
+
// the per-run stateRoot from (cwd, runId) via createRunPaths (same helper
|
|
423
|
+
// resolveRunStateRoot uses internally), so each run locks its own
|
|
424
|
+
// `<cwd>/.crew/state/runs/<runId>/run.lock`. Matches locks-race.test.ts.
|
|
425
|
+
const bootstrapStateRoot = createRunPaths(cwd, runId).stateRoot;
|
|
415
426
|
let loaded: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
416
427
|
try {
|
|
417
428
|
loaded = withRunLockSync(
|
|
418
|
-
{ stateRoot:
|
|
429
|
+
{ stateRoot: bootstrapStateRoot, runId, cwd } as TeamRunManifest,
|
|
419
430
|
() => loadRunManifestById(cwd, runId),
|
|
420
431
|
{ staleMs: 30_000 },
|
|
421
432
|
);
|
|
@@ -16,7 +16,11 @@ export interface ProcessLiveness {
|
|
|
16
16
|
*/
|
|
17
17
|
const ORPHANED_ACTIVE_RUN_MS = 2 * 60 * 1000;
|
|
18
18
|
/** How long a completed run stays visible in the widget after completion. */
|
|
19
|
-
const COMPLETED_VISIBILITY_GRACE_MS =
|
|
19
|
+
const COMPLETED_VISIBILITY_GRACE_MS = 8_000;
|
|
20
|
+
/** Errors (failed/cancelled) linger far longer so a failed run leaves a visible
|
|
21
|
+
* trace in the crew widget for ~10 min (F-5). Successful completions vanish
|
|
22
|
+
* quickly to keep the widget quiet. */
|
|
23
|
+
const ERROR_VISIBILITY_GRACE_MS = 10 * 60 * 1000;
|
|
20
24
|
/** Maximum age (ms) for an active run before it's considered stale.
|
|
21
25
|
* After this time, PID-only liveness is unreliable due to PID recycling. */
|
|
22
26
|
const STALE_ACTIVE_RUN_MS = 30 * 60 * 1000;
|
|
@@ -120,12 +124,13 @@ export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord
|
|
|
120
124
|
}
|
|
121
125
|
// Grace period: show completed runs for a few seconds so users see the result.
|
|
122
126
|
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
127
|
+
const grace = run.status === "completed" ? COMPLETED_VISIBILITY_GRACE_MS : ERROR_VISIBILITY_GRACE_MS;
|
|
123
128
|
const lastAgentActivity = agents.reduce<number>((max, agent) => {
|
|
124
129
|
const ts = agent.completedAt ?? agent.startedAt;
|
|
125
130
|
const parsed = ts ? new Date(ts).getTime() : 0;
|
|
126
131
|
return Number.isFinite(parsed) && parsed > max ? parsed : max;
|
|
127
132
|
}, new Date(run.updatedAt).getTime());
|
|
128
|
-
if (Number.isFinite(lastAgentActivity) && now - lastAgentActivity <
|
|
133
|
+
if (Number.isFinite(lastAgentActivity) && now - lastAgentActivity < grace) return true;
|
|
129
134
|
return false;
|
|
130
135
|
}
|
|
131
136
|
if (!isActiveRunStatus(run.status)) return false;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { isSensitivePath } from "./sensitive-paths.ts";
|
|
2
|
-
|
|
3
1
|
export type RolePermissionMode = "read_only" | "workspace_write" | "danger_full_access" | "explicit_confirm";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// Read-only roles: cannot mutate files/source. `verifier` is NOT here — it runs
|
|
4
|
+
// tests (bash + cache writes) so it is a WRITE role (F4). `planner` stays
|
|
5
|
+
// read-only to preserve the plan-approval gate boundary (F3).
|
|
6
|
+
const READ_ONLY_ROLES = new Set(["explorer", "reviewer", "security-reviewer", "analyst", "critic", "planner"]);
|
|
7
|
+
const WRITE_ROLES = new Set(["executor", "test-engineer", "writer", "verifier"]);
|
|
9
8
|
export interface PermissionCheckResult {
|
|
10
9
|
allowed: boolean;
|
|
11
10
|
mode: RolePermissionMode;
|
|
@@ -18,21 +17,6 @@ export function permissionForRole(role: string): RolePermissionMode {
|
|
|
18
17
|
return "workspace_write";
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export function isReadOnlyCommand(command: string): boolean {
|
|
22
|
-
const first = command.trim().split(/\s+/)[0]?.split(/[\\/]/).pop() ?? "";
|
|
23
|
-
return READ_ONLY_COMMANDS.has(first) && !/\s(-i|--in-place)\b|\s>{1,2}\s|\brm\b|\bmv\b|\bcp\b|\b(?:npm|pnpm|yarn|bun)\s+(install|add|ci|remove)\b|\bgit\s+(commit|push|merge|rebase|reset|checkout|clean)\b/.test(command);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function checkRolePermission(role: string, command: string, filePath?: string): PermissionCheckResult {
|
|
27
|
-
const mode = permissionForRole(role);
|
|
28
|
-
// Also block access to known sensitive paths even for read-only commands
|
|
29
|
-
if (filePath && isSensitivePath(filePath)) {
|
|
30
|
-
return { allowed: false, mode, reason: `Path '${filePath}' is sensitive (credentials, SSH keys, etc.) — access denied for all roles.` };
|
|
31
|
-
}
|
|
32
|
-
if (mode === "read_only" && !isReadOnlyCommand(command)) return { allowed: false, mode, reason: `Role '${role}' is read-only and command may modify state.` };
|
|
33
|
-
return { allowed: true, mode };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
20
|
export function currentCrewRole(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
37
21
|
return env.PI_CREW_ROLE?.trim() || env.PI_TEAMS_ROLE?.trim() || undefined;
|
|
38
22
|
}
|
|
@@ -30,6 +30,7 @@ function readOnlyRoleInstructions(role: string): string {
|
|
|
30
30
|
"- Do not use shell redirects, heredocs, in-place edits, package installs, git commit/merge/rebase/reset/checkout, or other state-mutating commands.",
|
|
31
31
|
"- If implementation changes are needed, report exact recommendations instead of applying them.",
|
|
32
32
|
"- Prefer read/grep/find/listing tools and read-only git inspection commands.",
|
|
33
|
+
"- Your final RESULT TEXT is persisted automatically by the runner (as a result artifact and, if the step declares `output:`, to a shared file). To deliver a plan, report, or findings, EMIT THEM AS TEXT in your final result — do NOT try to write a file yourself.",
|
|
33
34
|
].join("\n");
|
|
34
35
|
}
|
|
35
36
|
|