pi-crew 0.6.1 → 0.6.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 +194 -0
- package/README.md +81 -33
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +2 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +213 -35
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +27 -2
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +9 -4
- package/src/extension/registration/team-tool.ts +17 -10
- package/src/extension/registration/viewers.ts +2 -2
- package/src/extension/team-tool/api.ts +3 -3
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +64 -14
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool-types.ts +2 -0
- package/src/extension/team-tool.ts +173 -46
- package/src/hooks/registry.ts +77 -13
- package/src/hooks/types.ts +9 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/async-runner.ts +35 -7
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +9 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +164 -37
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +2 -0
- package/src/runtime/diagnostic-export.ts +1 -1
- package/src/runtime/dynamic-script-runner.ts +10 -7
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +1 -1
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +17 -2
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/skill-effectiveness.ts +105 -62
- package/src/runtime/skill-instructions.ts +110 -32
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +251 -57
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +92 -70
- package/src/runtime/team-runner.ts +186 -20
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +254 -70
- package/src/state/health-store.ts +71 -0
- package/src/state/locks.ts +102 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/session-state-map.ts +51 -0
- package/src/state/state-store.ts +330 -43
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +2 -2
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/guards.ts +110 -0
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +281 -10
- package/src/worktree/cleanup.ts +112 -24
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# pi-crew UI Optimization & Beautification Plan
|
|
2
|
+
|
|
3
|
+
> Based on deep analysis of oh-my-pi patterns + pi-crew current UI state.
|
|
4
|
+
> Date: 2026-06-12
|
|
5
|
+
> Status: Draft — awaiting approval
|
|
6
|
+
|
|
7
|
+
## Current State
|
|
8
|
+
|
|
9
|
+
### pi-crew UI (~7,800 lines across 45 files)
|
|
10
|
+
|
|
11
|
+
| Component | Lines | Status |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `run-snapshot-cache.ts` | 827 | Complex, over-engineered |
|
|
14
|
+
| `settings-overlay.ts` | 723 | Functional |
|
|
15
|
+
| `crew-widget.ts` | 544 | **High complexity, hard to read** |
|
|
16
|
+
| `run-dashboard.ts` | 536 | Dense, many responsibilities |
|
|
17
|
+
| `mascot.ts` | 444 | Cute but bloated |
|
|
18
|
+
| `tool-render.ts` | 380 | **Needs pattern overhaul** |
|
|
19
|
+
| `theme-adapter.ts` | 190 | Defensive but verbose |
|
|
20
|
+
|
|
21
|
+
### Key Problems
|
|
22
|
+
|
|
23
|
+
1. **No theme color consistency** — `CrewThemeColor` has ~22 slots vs oh-my-pi's 40+
|
|
24
|
+
2. **Tool rendering reimplements logic** — pi-brief's delegate-execute pattern is cleaner
|
|
25
|
+
3. **Widget is monolithic** — 544 lines doing state management, rendering, caching, and formatting
|
|
26
|
+
4. **No structured interaction** — `respond` action is free-text only; no `ask_user`-style schema
|
|
27
|
+
5. **Raw ANSI codes in places** — mixing `theme.fg()` with `\x1b[38;5;2m`
|
|
28
|
+
6. **No brief/compact mode** — all output is verbose, no toggle for condensed display
|
|
29
|
+
7. **Checkpoint display is text-heavy** — run history shows full text vs compact stats
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Phase 1: Theme System Upgrade
|
|
34
|
+
|
|
35
|
+
**Priority**: HIGH — foundation for all other UI work
|
|
36
|
+
**Effort**: 2-3 days
|
|
37
|
+
**Risk**: LOW (additive changes, no breaking API)
|
|
38
|
+
|
|
39
|
+
### 1.1 Expand Theme Color Slots
|
|
40
|
+
|
|
41
|
+
**Current** (`src/ui/theme-adapter.ts`):
|
|
42
|
+
```typescript
|
|
43
|
+
export type CrewThemeColor =
|
|
44
|
+
| "accent" | "border" | "borderAccent" | "borderMuted"
|
|
45
|
+
| "success" | "error" | "warning" | "muted" | "dim" | "text"
|
|
46
|
+
| "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext"
|
|
47
|
+
| "syntaxKeyword" | "syntaxString" | "syntaxNumber"
|
|
48
|
+
| "syntaxComment" | "syntaxFunction" | "syntaxVariable"
|
|
49
|
+
| "syntaxType" | "syntaxOperator" | "syntaxPunctuation"
|
|
50
|
+
| "mdCodeBlock";
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Target** — add from oh-my-pi's 40+ slots:
|
|
54
|
+
```typescript
|
|
55
|
+
export type CrewThemeColor =
|
|
56
|
+
// Existing (keep)
|
|
57
|
+
| "accent" | "border" | "borderAccent" | "borderMuted"
|
|
58
|
+
| "success" | "error" | "warning" | "muted" | "dim" | "text"
|
|
59
|
+
| "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext"
|
|
60
|
+
| "syntaxKeyword" | "syntaxString" | "syntaxNumber"
|
|
61
|
+
| "syntaxComment" | "syntaxFunction" | "syntaxVariable"
|
|
62
|
+
| "syntaxType" | "syntaxOperator" | "syntaxPunctuation"
|
|
63
|
+
| "mdCodeBlock"
|
|
64
|
+
// NEW — message display
|
|
65
|
+
| "userMessageText" | "customMessageLabel"
|
|
66
|
+
// NEW — tool rendering
|
|
67
|
+
| "toolTitle" // already used in tool-render.ts but missing from type
|
|
68
|
+
| "toolOutput"
|
|
69
|
+
| "toolPending" | "toolSuccess" | "toolError"
|
|
70
|
+
// NEW — markdown
|
|
71
|
+
| "mdHeading" | "mdLink" | "mdCode" | "mdQuote" | "mdHr" | "mdListBullet"
|
|
72
|
+
// NEW — thinking gradient (6 levels)
|
|
73
|
+
| "thinkingOff" | "thinkingMinimal" | "thinkingLow"
|
|
74
|
+
| "thinkingMedium" | "thinkingHigh" | "thinkingXhigh"
|
|
75
|
+
// NEW — special
|
|
76
|
+
| "bashMode" | "thinkingText";
|
|
77
|
+
|
|
78
|
+
export type CrewThemeBg =
|
|
79
|
+
// Existing
|
|
80
|
+
| "selectedBg" | "userMessageBg" | "toolPendingBg" | "toolSuccessBg" | "toolErrorBg"
|
|
81
|
+
// NEW
|
|
82
|
+
| "customMessageBg";
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Files to change**: `src/ui/theme-adapter.ts`
|
|
86
|
+
|
|
87
|
+
### 1.2 Theme Color Fallback Map
|
|
88
|
+
|
|
89
|
+
Add default ANSI values for all new slots so widgets work even with minimal themes:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const THEME_FALLBACKS: Record<CrewThemeColor, string> = {
|
|
93
|
+
toolTitle: "\x1b[36m", // cyan
|
|
94
|
+
toolOutput: "\x1b[38;5;245m", // gray
|
|
95
|
+
toolPending: "\x1b[38;5;240m",
|
|
96
|
+
toolSuccess: "\x1b[32m",
|
|
97
|
+
toolError: "\x1b[31m",
|
|
98
|
+
mdHeading: "\x1b[33m", // yellow
|
|
99
|
+
mdLink: "\x1b[35m", // indigo
|
|
100
|
+
mdCode: "\x1b[32m", // green
|
|
101
|
+
// ... all new slots
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 1.3 Remove Raw ANSI from Business Logic
|
|
106
|
+
|
|
107
|
+
Find and replace all raw `\x1b[38;5;XXXm` in non-UI files with `theme.fg("slot", text)` calls.
|
|
108
|
+
|
|
109
|
+
**Files to audit**:
|
|
110
|
+
- `src/ui/tool-render.ts` — check for raw ANSI
|
|
111
|
+
- `src/extension/registration/commands.ts` — status line rendering
|
|
112
|
+
- `src/runtime/task-display.ts` — display formatting
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Phase 2: Tool Rendering Overhaul
|
|
117
|
+
|
|
118
|
+
**Priority**: HIGH — visible improvement to every tool call display
|
|
119
|
+
**Effort**: 2-3 days
|
|
120
|
+
**Risk**: MEDIUM (changes how tool calls look)
|
|
121
|
+
|
|
122
|
+
### 2.1 Adopt Rendering-Only Override Pattern
|
|
123
|
+
|
|
124
|
+
**From oh-my-pi's pi-brief**: Register tool rendering that delegates execution to original.
|
|
125
|
+
|
|
126
|
+
**Current pi-crew approach** (`src/ui/tool-render.ts`):
|
|
127
|
+
- 380 lines of monolithic rendering functions
|
|
128
|
+
- Separate `renderTeamToolCall`, `renderAgentToolCall`, `renderTeamToolResult`, `renderAgentToolResult`
|
|
129
|
+
- Each function handles both collapsed and expanded mode internally
|
|
130
|
+
|
|
131
|
+
**Target**: Split into focused renderers using a registry pattern:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// src/ui/tool-renderers/index.ts
|
|
135
|
+
export interface ToolRenderer {
|
|
136
|
+
renderCall(args: unknown, theme: CrewTheme, expanded: boolean): Component;
|
|
137
|
+
renderResult(result: unknown, theme: CrewTheme, expanded: boolean): Component;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/ui/tool-renderers/team-renderer.ts
|
|
141
|
+
export const teamToolRenderer: ToolRenderer = {
|
|
142
|
+
renderCall(args, theme, expanded) { ... },
|
|
143
|
+
renderResult(result, theme, expanded) { ... },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/ui/tool-renderers/agent-renderer.ts
|
|
147
|
+
export const agentToolRenderer: ToolRenderer = { ... };
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2.2 Compact Brief Mode for Tool Output
|
|
151
|
+
|
|
152
|
+
**From pi-brief**: Add a configurable "brief" toggle that shows one-line summaries.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// New: src/ui/tool-renderers/brief-mode.ts
|
|
156
|
+
export function briefResult(result: ToolResult, theme: CrewTheme): string {
|
|
157
|
+
// read → "→ 142 lines"
|
|
158
|
+
// bash → "→ done" or "→ 12 lines"
|
|
159
|
+
// edit → "→ edited +3 -1"
|
|
160
|
+
// write → "→ written"
|
|
161
|
+
// team → "→ 3/3 tasks · 1.2m · 45k tok"
|
|
162
|
+
// agent → "✓ explorer · 8 tools · 23.4s"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Add `/crew-brief on|off` command and persist state via `pi.appendEntry()`.
|
|
167
|
+
|
|
168
|
+
### 2.3 Team Tool Result — Compact Stats Display
|
|
169
|
+
|
|
170
|
+
**From pi-rewind's checkpoint display**: Show goal + task stats in one line.
|
|
171
|
+
|
|
172
|
+
**Current**:
|
|
173
|
+
```
|
|
174
|
+
team action='run' (implementation) · status=completed · runId=team_2026... · goal="Investigate failing..."
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Target** (compact):
|
|
178
|
+
```
|
|
179
|
+
✓ team/implementation · 3/3 tasks · 1.2m · ↑45k ↓12k · $0.042
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With expanded (Ctrl+O):
|
|
183
|
+
```
|
|
184
|
+
✓ team/implementation · 3/3 tasks · 1.2m · ↑45k ↓12k · $0.042
|
|
185
|
+
├─ ✓ explorer · 8 tools · 23.4s · ↑12k ↓4k
|
|
186
|
+
├─ ✓ executor · 15 tools · 41.2s · ↑22k ↓6k
|
|
187
|
+
└─ ✓ verifier · 5 tools · 12.1s · ↑11k ↓2k
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Phase 3: Widget Refactor
|
|
193
|
+
|
|
194
|
+
**Priority**: MEDIUM — internal cleanup, same visual output
|
|
195
|
+
**Effort**: 3-4 days
|
|
196
|
+
**Risk**: MEDIUM (core display component)
|
|
197
|
+
|
|
198
|
+
### 3.1 Split crew-widget.ts into Modules
|
|
199
|
+
|
|
200
|
+
**Current**: 544 lines monolith doing everything.
|
|
201
|
+
|
|
202
|
+
**Target structure**:
|
|
203
|
+
```
|
|
204
|
+
src/ui/widget/
|
|
205
|
+
├── index.ts # Public API: updateCrewWidget, stopCrewWidget
|
|
206
|
+
├── widget-component.ts # CrewWidgetComponent class (~100 lines)
|
|
207
|
+
├── widget-model.ts # Data fetching + caching (~100 lines)
|
|
208
|
+
├── widget-renderer.ts # Line building + colorizing (~150 lines)
|
|
209
|
+
├── widget-formatters.ts # formatTokens, formatDuration, agentStats (~100 lines)
|
|
210
|
+
└── widget-types.ts # Shared types
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 3.2 Adopt SessionStateMap Pattern
|
|
214
|
+
|
|
215
|
+
**From oh-my-pi**: Generic session-scoped state container instead of scattered maps.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// src/state/session-state-map.ts (new)
|
|
219
|
+
export class SessionStateMap<T> {
|
|
220
|
+
private map = new Map<string, T>();
|
|
221
|
+
getOrUndefined(sessionId: string): T | undefined { ... }
|
|
222
|
+
set(sessionId: string, value: T): void { ... }
|
|
223
|
+
delete(sessionId: string): void { ... }
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Use this in widget, dashboard, and live-agent-manager instead of separate `Map<string, ...>` instances.
|
|
228
|
+
|
|
229
|
+
### 3.3 Render Coalescing Optimization
|
|
230
|
+
|
|
231
|
+
The widget currently rebuilds signature strings on every render call. Optimize:
|
|
232
|
+
- Pre-compute signatures when data changes, not on render
|
|
233
|
+
- Use a simple counter-based invalidation instead of string concatenation
|
|
234
|
+
- Eliminate `this.cacheSignature` string comparison in hot path
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Phase 4: Dashboard Polish
|
|
239
|
+
|
|
240
|
+
**Priority**: MEDIUM
|
|
241
|
+
**Effort**: 3-4 days
|
|
242
|
+
**Risk**: LOW
|
|
243
|
+
|
|
244
|
+
### 4.1 Run History — Compact Checkpoint Style
|
|
245
|
+
|
|
246
|
+
**From pi-rewind**: Show runs with file-change statistics.
|
|
247
|
+
|
|
248
|
+
**Current** (status command output):
|
|
249
|
+
```
|
|
250
|
+
Run: team_20260612100313...
|
|
251
|
+
Status: completed
|
|
252
|
+
Team: implementation
|
|
253
|
+
Goal: Fix failing tests
|
|
254
|
+
Tasks: 3/3
|
|
255
|
+
Duration: 5m23s
|
|
256
|
+
Tokens: 45.2k
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Target** (compact):
|
|
260
|
+
```
|
|
261
|
+
✓ team_...3d7f implementation · Fix failing tests
|
|
262
|
+
3/3 tasks · 5m23s · ↑45k ↓12k
|
|
263
|
+
14 files +342 -87
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 4.2 Agent Progress — Thinking Level Visualization
|
|
267
|
+
|
|
268
|
+
**From oh-my-pi's thinking gradient**: Show thinking intensity with color gradient.
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
function thinkingColor(level: number, theme: CrewTheme): CrewThemeColor {
|
|
272
|
+
// level 0-5 → off, minimal, low, medium, high, xhigh
|
|
273
|
+
return ["thinkingOff", "thinkingMinimal", "thinkingLow",
|
|
274
|
+
"thinkingMedium", "thinkingHigh", "thinkingXhigh"][level];
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Apply to agent activity line when agent is "thinking..." (no tool call, just LLM processing).
|
|
279
|
+
|
|
280
|
+
### 4.3 Dashboard Pane Improvements
|
|
281
|
+
|
|
282
|
+
**Agents pane**: Add compact/expanded toggle per agent
|
|
283
|
+
**Progress pane**: Use brief mode rendering from Phase 2
|
|
284
|
+
**Mailbox pane**: Add structured response schema option
|
|
285
|
+
**Health pane**: Color-code issues by severity
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Phase 5: Structured Interaction
|
|
290
|
+
|
|
291
|
+
**Priority**: MEDIUM
|
|
292
|
+
**Effort**: 4-5 days
|
|
293
|
+
**Risk**: MEDIUM (new feature)
|
|
294
|
+
|
|
295
|
+
### 5.1 Structured Respond Action
|
|
296
|
+
|
|
297
|
+
**From pi-clarify**: Add schema validation to `respond` action.
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// In team-tool/respond.ts
|
|
301
|
+
interface RespondOptions {
|
|
302
|
+
message: string;
|
|
303
|
+
// NEW: structured answer option
|
|
304
|
+
answer?: {
|
|
305
|
+
type: "select" | "text" | "confirm";
|
|
306
|
+
value: string | boolean;
|
|
307
|
+
label?: string;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 5.2 Secret Rejection in Tool Inputs
|
|
313
|
+
|
|
314
|
+
**From pi-clarify's SECRET_WORDS**: Add to all tool inputs that accept user text.
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const SECRET_PATTERNS = [
|
|
318
|
+
"api key", "apikey", "auth token", "cookie", "credential",
|
|
319
|
+
"password", "private key", "secret", "token",
|
|
320
|
+
];
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Already partially done in `env-filter.ts`, but should extend to `goal`, `message`, `task` parameters.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Phase 6: Code Quality from oh-my-pi
|
|
328
|
+
|
|
329
|
+
**Priority**: LOW-MEDIUM (foundation for long-term)
|
|
330
|
+
**Effort**: 2-3 days
|
|
331
|
+
**Risk**: LOW
|
|
332
|
+
|
|
333
|
+
### 6.1 Type Guard Library
|
|
334
|
+
|
|
335
|
+
**From runtime-core**: Create `src/utils/guards.ts` with systematic type guards.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// src/utils/guards.ts
|
|
339
|
+
export function isRecord(value: unknown): value is Record<string, unknown> { ... }
|
|
340
|
+
export function isString(value: unknown): value is string { ... }
|
|
341
|
+
export function isNumber(value: unknown): value is number { ... }
|
|
342
|
+
export function isNonEmptyString(value: unknown): value is string { ... }
|
|
343
|
+
export function isArrayOf<T>(guard: (v: unknown) => v is T): (v: unknown) => v is readonly T[] { ... }
|
|
344
|
+
export function getStringField(value: unknown, key: string): string | undefined { ... }
|
|
345
|
+
export function getNumberField(value: unknown, key: number): number | undefined { ... }
|
|
346
|
+
export function errorMessage(err: unknown): string { ... }
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Replace scattered `typeof x === "string"` checks across codebase.
|
|
350
|
+
|
|
351
|
+
### 6.2 No-Any Lint Rule
|
|
352
|
+
|
|
353
|
+
Add ESLint rule to enforce `unknown` over `any`:
|
|
354
|
+
|
|
355
|
+
```javascript
|
|
356
|
+
// .eslintrc
|
|
357
|
+
{
|
|
358
|
+
"rules": {
|
|
359
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
360
|
+
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
361
|
+
"@typescript-eslint/no-unsafe-call": "error"
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 6.3 mkdir-Based Locking (Optional)
|
|
367
|
+
|
|
368
|
+
**From pi-checkpoint**: Replace JSON token-based locks with `mkdir` atomic locks.
|
|
369
|
+
|
|
370
|
+
This is a bigger change — could be Phase 7 if needed.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Implementation Order & Timeline
|
|
375
|
+
|
|
376
|
+
| Phase | Duration | Depends On | Impact |
|
|
377
|
+
|---|---|---|---|
|
|
378
|
+
| **Phase 1**: Theme System | 2-3 days | Nothing | Foundation for all UI |
|
|
379
|
+
| **Phase 2**: Tool Rendering | 2-3 days | Phase 1 | Every tool call looks better |
|
|
380
|
+
| **Phase 3**: Widget Refactor | 3-4 days | Phase 1 | Internal cleanup |
|
|
381
|
+
| **Phase 4**: Dashboard Polish | 3-4 days | Phase 1, 2 | Visible improvement |
|
|
382
|
+
| **Phase 5**: Structured Interaction | 4-5 days | Phase 2 | New feature |
|
|
383
|
+
| **Phase 6**: Code Quality | 2-3 days | Nothing | Long-term maintainability |
|
|
384
|
+
|
|
385
|
+
**Total estimated**: 16-22 days (can parallelize Phase 1+6, Phase 2+3)
|
|
386
|
+
|
|
387
|
+
### Recommended Execution
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
Week 1: Phase 1 (Theme) + Phase 6 (Code Quality) [parallel]
|
|
391
|
+
Week 2: Phase 2 (Tool Rendering)
|
|
392
|
+
Week 3: Phase 3 (Widget) + Phase 4 (Dashboard) [sequential]
|
|
393
|
+
Week 4: Phase 5 (Structured Interaction) + polish
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Success Metrics
|
|
399
|
+
|
|
400
|
+
| Metric | Current | Target |
|
|
401
|
+
|---|---|---|
|
|
402
|
+
| Theme color slots | 22 | 40+ |
|
|
403
|
+
| Raw ANSI in business logic | ~15 instances | 0 |
|
|
404
|
+
| Widget lines of code | 544 | ~150 (per module) |
|
|
405
|
+
| Tool result compact display | N/A | All 7 tool types |
|
|
406
|
+
| Structured respond | Free text only | Schema-validated |
|
|
407
|
+
| `any` in production code | ~40 instances | 0 |
|
|
408
|
+
| Type guard reuse | Scattered | Centralized in guards.ts |
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Risks & Mitigations
|
|
413
|
+
|
|
414
|
+
| Risk | Mitigation |
|
|
415
|
+
|---|---|
|
|
416
|
+
| Theme slot additions break existing themes | All new slots have fallback ANSI values |
|
|
417
|
+
| Widget refactor breaks live updates | Keep same public API, refactor internals only |
|
|
418
|
+
| Brief mode confuses users | Default OFF, toggle via `/crew-brief on` |
|
|
419
|
+
| Structured respond breaks existing workflows | `message` field still accepts free text; `answer` is optional |
|
|
420
|
+
| No-any lint produces too many errors | Incremental: start with new files, then expand |
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Files to Create/Modify
|
|
425
|
+
|
|
426
|
+
### New Files
|
|
427
|
+
- `src/ui/tool-renderers/index.ts`
|
|
428
|
+
- `src/ui/tool-renderers/team-renderer.ts`
|
|
429
|
+
- `src/ui/tool-renderers/agent-renderer.ts`
|
|
430
|
+
- `src/ui/tool-renderers/brief-mode.ts`
|
|
431
|
+
- `src/ui/widget/index.ts`
|
|
432
|
+
- `src/ui/widget/widget-component.ts`
|
|
433
|
+
- `src/ui/widget/widget-model.ts`
|
|
434
|
+
- `src/ui/widget/widget-renderer.ts`
|
|
435
|
+
- `src/ui/widget/widget-formatters.ts`
|
|
436
|
+
- `src/ui/widget/widget-types.ts`
|
|
437
|
+
- `src/state/session-state-map.ts`
|
|
438
|
+
- `src/utils/guards.ts`
|
|
439
|
+
|
|
440
|
+
### Modified Files
|
|
441
|
+
- `src/ui/theme-adapter.ts` — expand color types + fallbacks
|
|
442
|
+
- `src/ui/tool-render.ts` — slim down, delegate to renderers
|
|
443
|
+
- `src/ui/crew-widget.ts` — split into widget/ modules
|
|
444
|
+
- `src/ui/run-dashboard.ts` — compact display + thinking gradient
|
|
445
|
+
- `src/ui/status-colors.ts` — add new status colors
|
|
446
|
+
- `src/extension/team-tool/respond.ts` — structured response option
|
|
447
|
+
- `src/extension/registration/commands.ts` — add `/crew-brief` command
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"ajv": "^8.20.0",
|
|
85
85
|
"cli-highlight": "^2.1.11",
|
|
86
86
|
"diff": "^5.2.0",
|
|
87
|
-
"jiti": "^2.
|
|
87
|
+
"jiti": "^2.7.0",
|
|
88
88
|
"typebox": "^1.1.38"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
package/src/config/config.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PiTeamsConfigSchema,
|
|
9
9
|
} from "../schema/config-schema.ts";
|
|
10
10
|
import { withFileLockSync } from "../state/locks.ts";
|
|
11
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
11
12
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
12
13
|
import { projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
|
|
13
14
|
import { suggestConfigKey } from "./suggestions.ts";
|
|
@@ -79,7 +80,17 @@ function resolveHomeDir(): string {
|
|
|
79
80
|
// directory (e.g. withIsolatedHome) set PI_TEAMS_HOME to a tmp dir
|
|
80
81
|
// under /tmp; we skip the check in test environments (NODE_ENV=test)
|
|
81
82
|
// so existing tests don't break.
|
|
82
|
-
if (process.env.
|
|
83
|
+
if (process.env.PI_CREW_SKIP_HOME_CHECK === "1") {
|
|
84
|
+
return envValue;
|
|
85
|
+
}
|
|
86
|
+
// NOTE: NODE_ENV=test bypass is intentional for test isolation only.
|
|
87
|
+
// It allows tests to use isolated temporary directories (e.g. withIsolatedHome
|
|
88
|
+
// sets PI_TEAMS_HOME to /tmp). This is NOT a security boundary — tests that
|
|
89
|
+
// need the validation skipped should set PI_CREW_SKIP_HOME_CHECK=1 explicitly.
|
|
90
|
+
// WARNING: Tests must NOT rely on PI_TEAMS_HOME for security boundaries in
|
|
91
|
+
// test environments. Any test verifying path-restriction behavior should set
|
|
92
|
+
// PI_CREW_SKIP_HOME_CHECK=1 and validate the path directly.
|
|
93
|
+
if (process.env.NODE_ENV === "test") {
|
|
83
94
|
return envValue;
|
|
84
95
|
}
|
|
85
96
|
try {
|
|
@@ -481,10 +492,22 @@ function mergeConfig(
|
|
|
481
492
|
};
|
|
482
493
|
if (Object.keys(merged.otlp.headers ?? {}).length === 0)
|
|
483
494
|
delete merged.otlp.headers;
|
|
484
|
-
// Validate OTLP headers for injection attacks
|
|
495
|
+
// Validate OTLP headers for injection attacks:
|
|
496
|
+
// - Check top-level keys for dangerous prototype pollution patterns
|
|
497
|
+
// - Block all control characters (except tab=0x09, newline=0x0A) to prevent
|
|
498
|
+
// header injection via CR/LF/zero-byte/etc.
|
|
485
499
|
const invalidHeaders: string[] = [];
|
|
486
500
|
for (const [k, v] of Object.entries(merged.otlp.headers ?? {})) {
|
|
487
|
-
|
|
501
|
+
// Check top-level key for dangerous names (only top-level keys are checked)
|
|
502
|
+
const checkKey = (key: string): boolean => {
|
|
503
|
+
const lowerKey = key.toLowerCase();
|
|
504
|
+
if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) return true;
|
|
505
|
+
return false;
|
|
506
|
+
};
|
|
507
|
+
if (checkKey(k)) { invalidHeaders.push(k); continue; }
|
|
508
|
+
// Block any control characters except tab (0x09) in values
|
|
509
|
+
const valStr = String(v);
|
|
510
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(valStr)) { invalidHeaders.push(k); }
|
|
488
511
|
}
|
|
489
512
|
if (invalidHeaders.length > 0) {
|
|
490
513
|
delete merged.otlp.headers;
|
|
@@ -511,10 +534,57 @@ const LIMIT_CEILINGS = {
|
|
|
511
534
|
runtimeGraceTurns: 1_000,
|
|
512
535
|
} as const;
|
|
513
536
|
|
|
514
|
-
|
|
537
|
+
/**
|
|
538
|
+
* Keys that could allow prototype pollution if merged into plain objects.
|
|
539
|
+
* NOTE: This set is comprehensive for ES2023 and earlier. When upgrading JavaScript
|
|
540
|
+
* versions, verify whether new dangerous Object.prototype properties have been added
|
|
541
|
+
* that could enable prototype pollution attacks.
|
|
542
|
+
*/
|
|
543
|
+
const DANGEROUS_OBJECT_KEYS = new Set([
|
|
544
|
+
"__proto__",
|
|
545
|
+
"constructor",
|
|
546
|
+
"prototype",
|
|
547
|
+
"hasOwnProperty",
|
|
548
|
+
"toString",
|
|
549
|
+
"valueOf",
|
|
550
|
+
"isPrototypeOf",
|
|
551
|
+
"propertyIsEnumerable",
|
|
552
|
+
"toLocaleString",
|
|
553
|
+
"__defineGetter__",
|
|
554
|
+
"__defineSetter__",
|
|
555
|
+
"__lookupGetter__",
|
|
556
|
+
"__lookupSetter__",
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Strips dangerous Object.prototype keys from an object.
|
|
561
|
+
* Returns a new object built with Object.create(null) to prevent
|
|
562
|
+
* prototype pollution attacks.
|
|
563
|
+
*/
|
|
564
|
+
function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
565
|
+
const sanitized: Record<string, unknown> = Object.create(null);
|
|
566
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
567
|
+
// Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
|
|
568
|
+
const lowerKey = key.toLowerCase();
|
|
569
|
+
if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) continue;
|
|
570
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
571
|
+
sanitized[key] = sanitizeObject(value as Record<string, unknown>);
|
|
572
|
+
} else {
|
|
573
|
+
sanitized[key] = value;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return sanitized;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
515
580
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
516
581
|
return undefined;
|
|
517
|
-
|
|
582
|
+
// Defensive: create a sanitized copy to prevent prototype pollution.
|
|
583
|
+
// Uses Object.create(null) so the result has no prototype chain.
|
|
584
|
+
// WARNING: The returned object has no prototype methods (no hasOwnProperty,
|
|
585
|
+
// toString, etc.). Use Object.hasOwn(obj, key) or
|
|
586
|
+
// Object.prototype.hasOwnProperty.call(obj, key) for property checks.
|
|
587
|
+
return sanitizeObject(value as Record<string, unknown>);
|
|
518
588
|
}
|
|
519
589
|
|
|
520
590
|
function parseWithSchema<T extends TSchema>(
|
|
@@ -1190,7 +1260,29 @@ function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
|
|
|
1190
1260
|
|
|
1191
1261
|
function readConfigRecord(filePath: string): Record<string, unknown> {
|
|
1192
1262
|
if (!fs.existsSync(filePath)) return {};
|
|
1193
|
-
|
|
1263
|
+
// Defense-in-depth: reject config files larger than 10 MB before parsing.
|
|
1264
|
+
// This prevents memory exhaustion and blocks deeply nested JSON that could
|
|
1265
|
+
// cause stack overflow during parsing.
|
|
1266
|
+
const MAX_CONFIG_SIZE = 10 * 1024 * 1024;
|
|
1267
|
+
const stat = fs.statSync(filePath);
|
|
1268
|
+
if (stat.size > MAX_CONFIG_SIZE) {
|
|
1269
|
+
logInternalError(
|
|
1270
|
+
"config.file-too-large",
|
|
1271
|
+
new Error(`config file exceeds ${MAX_CONFIG_SIZE} bytes`),
|
|
1272
|
+
`path=${filePath}; size=${stat.size}`,
|
|
1273
|
+
);
|
|
1274
|
+
return {};
|
|
1275
|
+
}
|
|
1276
|
+
// Parse with depth limit to prevent stack overflow from deeply nested JSON.
|
|
1277
|
+
// Nesting beyond 100 levels is almost certainly an attack or malformed file.
|
|
1278
|
+
const MAX_JSON_DEPTH = 100;
|
|
1279
|
+
let depth = 0;
|
|
1280
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"), (_key, value) => {
|
|
1281
|
+
if (++depth > MAX_JSON_DEPTH) {
|
|
1282
|
+
throw new Error(`config JSON exceeds max depth ${MAX_JSON_DEPTH}`);
|
|
1283
|
+
}
|
|
1284
|
+
return value;
|
|
1285
|
+
}) as unknown;
|
|
1194
1286
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
1195
1287
|
return raw as Record<string, unknown>;
|
|
1196
1288
|
}
|
|
@@ -1244,7 +1336,7 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
1244
1336
|
const projectPath = projectConfigPath(cwd);
|
|
1245
1337
|
const projectConfig = readOptionalConfig(projectPath);
|
|
1246
1338
|
// SECURITY FIX: Merge project config FIRST, then user config on top.
|
|
1247
|
-
//
|
|
1339
|
+
// Precedence formula: merge(projectConfig, userConfig) = userConfig wins.
|
|
1248
1340
|
// Sensitive fields have already been sanitized by sanitizeProjectConfig.
|
|
1249
1341
|
let effectiveConfig = {};
|
|
1250
1342
|
if (projectConfig.exists) {
|
|
@@ -1257,6 +1349,7 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
1257
1349
|
...projectConfig.warnings,
|
|
1258
1350
|
...projectSafeConfig.warnings,
|
|
1259
1351
|
);
|
|
1352
|
+
// merge(base=projectConfig, override=userConfig) → override wins
|
|
1260
1353
|
effectiveConfig = mergeConfig(effectiveConfig, projectSafeConfig.config);
|
|
1261
1354
|
}
|
|
1262
1355
|
// User config always takes precedence over project config
|
|
@@ -1265,19 +1358,20 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
1265
1358
|
|
|
1266
1359
|
|
|
1267
1360
|
// `.pi/pi-crew.json` is the project-owned config file.
|
|
1268
|
-
//
|
|
1269
|
-
// This
|
|
1361
|
+
// Merge project config FIRST (base), then user config on top (override).
|
|
1362
|
+
// This ensures user preferences always take precedence over project settings.
|
|
1363
|
+
// Sensitive fields have already been sanitized by sanitizeProjectConfig.
|
|
1270
1364
|
const piCrewJsonPath = projectPiCrewJsonPath(cwd);
|
|
1271
1365
|
const piCrewJsonConfig = readOptionalConfig(piCrewJsonPath);
|
|
1272
1366
|
if (piCrewJsonConfig.exists) {
|
|
1273
1367
|
warnings.push(...piCrewJsonConfig.warnings);
|
|
1274
|
-
// Merge project config first, then user config on top
|
|
1275
1368
|
const projectPart = sanitizeProjectConfig(
|
|
1276
1369
|
piCrewJsonPath,
|
|
1277
1370
|
config,
|
|
1278
1371
|
piCrewJsonConfig.config,
|
|
1279
1372
|
);
|
|
1280
1373
|
warnings.push(...projectPart.warnings);
|
|
1374
|
+
// base=project config, override=user config → user wins
|
|
1281
1375
|
const mergedProject = mergeConfig(projectPart.config, config);
|
|
1282
1376
|
config = mergedProject;
|
|
1283
1377
|
paths.push(piCrewJsonPath);
|
|
@@ -1319,10 +1413,9 @@ export function updateConfig(
|
|
|
1319
1413
|
merged = parseConfig(raw);
|
|
1320
1414
|
}
|
|
1321
1415
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1322
|
-
|
|
1416
|
+
atomicWriteFile(
|
|
1323
1417
|
filePath,
|
|
1324
1418
|
`${JSON.stringify(merged, null, 2)}\n`,
|
|
1325
|
-
"utf-8",
|
|
1326
1419
|
);
|
|
1327
1420
|
return { path: filePath, config: merged };
|
|
1328
1421
|
});
|
|
@@ -1349,11 +1442,9 @@ export function updateAutonomousConfig(
|
|
|
1349
1442
|
? (current.autonomous as Record<string, unknown>)
|
|
1350
1443
|
: {};
|
|
1351
1444
|
current.autonomous = { ...currentAutonomous, ...patch };
|
|
1352
|
-
|
|
1353
|
-
fs.writeFileSync(
|
|
1445
|
+
atomicWriteFile(
|
|
1354
1446
|
filePath,
|
|
1355
1447
|
`${JSON.stringify(current, null, 2)}\n`,
|
|
1356
|
-
"utf-8",
|
|
1357
1448
|
);
|
|
1358
1449
|
return { path: filePath, config: parseConfig(current) };
|
|
1359
1450
|
});
|