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.
Files changed (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. 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.1",
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.6.1",
87
+ "jiti": "^2.7.0",
88
88
  "typebox": "^1.1.38"
89
89
  },
90
90
  "devDependencies": {
@@ -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.NODE_ENV === "test" || process.env.PI_CREW_SKIP_HOME_CHECK === "1") {
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 (newlines, CR, null bytes)
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
- if (/[\r\n\x00]/.test(String(v))) { invalidHeaders.push(k); }
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
- function asRecord(value: unknown): Record<string, unknown> | undefined {
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
- return value as Record<string, unknown>;
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
- const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
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
- // This ensures user preferences always take precedence over project settings.
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
- // SECURITY FIX: User config takes precedence over project-level `.pi/pi-crew.json`.
1269
- // This prevents malicious project configs from overriding user preferences.
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
- fs.writeFileSync(
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
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
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
  });