pi-crew 0.5.16 → 0.5.18

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 (54) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/package.json +3 -3
  3. package/skills/artifact-analysis-loop/SKILL.md +1 -0
  4. package/skills/async-worker-recovery/SKILL.md +1 -0
  5. package/skills/child-pi-spawning/SKILL.md +1 -0
  6. package/skills/context-artifact-hygiene/SKILL.md +1 -0
  7. package/skills/delegation-patterns/SKILL.md +1 -0
  8. package/skills/detection-pipeline-design/SKILL.md +2 -1
  9. package/skills/event-log-tracing/SKILL.md +1 -0
  10. package/skills/git-master/SKILL.md +1 -0
  11. package/skills/hunting-investigation-loop/SKILL.md +1 -0
  12. package/skills/incident-playbook-construction/SKILL.md +1 -0
  13. package/skills/iterative-audit/SKILL.md +1 -0
  14. package/skills/live-agent-lifecycle/SKILL.md +1 -0
  15. package/skills/mailbox-interactive/SKILL.md +1 -0
  16. package/skills/model-routing-context/SKILL.md +2 -1
  17. package/skills/multi-perspective-review/SKILL.md +1 -0
  18. package/skills/observability-reliability/SKILL.md +1 -0
  19. package/skills/orchestration/SKILL.md +2 -1
  20. package/skills/ownership-session-security/SKILL.md +1 -0
  21. package/skills/pi-extension-lifecycle/SKILL.md +3 -2
  22. package/skills/post-mortem/SKILL.md +1 -0
  23. package/skills/read-only-explorer/SKILL.md +1 -0
  24. package/skills/requirements-to-task-packet/SKILL.md +1 -0
  25. package/skills/resource-discovery-config/SKILL.md +2 -1
  26. package/skills/runtime-state-reader/SKILL.md +1 -0
  27. package/skills/safe-bash/SKILL.md +1 -0
  28. package/skills/scrutinize/SKILL.md +1 -0
  29. package/skills/secure-agent-orchestration-review/SKILL.md +1 -0
  30. package/skills/security-review/SKILL.md +1 -0
  31. package/skills/state-mutation-locking/SKILL.md +1 -0
  32. package/skills/systematic-debugging/SKILL.md +1 -0
  33. package/skills/threat-hypothesis-framework/SKILL.md +1 -0
  34. package/skills/ui-render-performance/SKILL.md +2 -1
  35. package/skills/verification-before-done/SKILL.md +1 -0
  36. package/skills/widget-rendering/SKILL.md +2 -1
  37. package/skills/workspace-isolation/SKILL.md +1 -0
  38. package/skills/worktree-isolation/SKILL.md +1 -0
  39. package/src/config/types.ts +1 -0
  40. package/src/extension/team-tool/orchestrate.ts +12 -4
  41. package/src/runtime/adaptive-plan.ts +18 -2
  42. package/src/runtime/child-pi.ts +17 -5
  43. package/src/runtime/dynamic-script-runner.ts +14 -1
  44. package/src/runtime/sandbox.ts +32 -1
  45. package/src/runtime/task-packet.ts +124 -0
  46. package/src/runtime/task-runner/prompt-builder.ts +4 -1
  47. package/src/runtime/task-runner.ts +1 -1
  48. package/src/schema/config-schema.ts +1 -0
  49. package/src/state/event-log.ts +7 -0
  50. package/src/tools/safe-bash-extension.ts +8 -49
  51. package/src/tools/safe-bash.ts +10 -0
  52. package/src/utils/project-detector.ts +3 -3
  53. package/src/workflows/workflow-config.ts +3 -0
  54. package/src/worktree/worktree-manager.ts +75 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,95 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.18] — Final Review Fixes (2026-06-03)
4
+
5
+ ### Highlights
6
+ - **4 HIGH issues fixed** from comprehensive final review of entire codebase
7
+ - CI now properly fails when tests fail (`npm test` exits non-zero)
8
+ - Sandbox prototype freeze scoped to VM context (no host process impact)
9
+ - Safe-bash extension delegates to core module (eliminated ReDoS regression)
10
+ - Shell injection eliminated in project-detector (`execSync` → `execFileSync`)
11
+
12
+ ### Fixes
13
+
14
+ #### HIGH: CI exit code
15
+ - `tsx --test` always exits 0 even with failing tests — masked regressions in CI
16
+ - Added `scripts/test-runner.mjs` wrapper that parses test output and exits 1 on failures
17
+ - Updated `test:unit` and `test:integration` npm scripts
18
+
19
+ #### HIGH: Sandbox prototype freeze scope
20
+ - `Object.freeze(Object.prototype)` in `WorkflowSandbox` constructor affected entire Node.js process
21
+ - Moved freeze inside VM context via `vm.runInContext()` — only freezes when sandbox is created, skipped in `NODE_ENV=test`
22
+ - Context object itself frozen (process-safe, only freezes our record)
23
+
24
+ #### HIGH: Shell injection risk in project-detector
25
+ - `execSync("git remote get-url origin")` passed through `/bin/sh -c` — any interpolated variable would be vulnerable
26
+ - Replaced with `execFileSync("git", ["remote", "get-url", "origin"])` — no shell interpretation
27
+
28
+ #### HIGH: ReDoS regression in safe-bash-extension
29
+ - Extension duplicated outdated regex patterns with O(n²) backtracking
30
+ - Refactored to import `isDangerous()` from `safe-bash.ts` (linear-time scanner)
31
+ - Eliminated code divergence between core and extension modules
32
+
33
+ ### Stats
34
+ - Test suite: 2698 pass + 1 skip, 0 fail
35
+ - TypeScript: 0 errors
36
+ - Files changed: 5
37
+ - Security issues fixed: 4 HIGH
38
+
39
+ ## [0.5.17] — Security Hardening + ECC Patterns + Skill Review (2026-06-03)
40
+
41
+ ### Highlights
42
+ - **3 CRITICAL security fixes**: path traversal, sandbox escape, executeUnchecked bypass
43
+ - **3 HIGH security fixes**: allowPatterns bypass, safe-bash fallback message, mock mode
44
+ - **3 MEDIUM security fixes**: home hooks visibility, API keys documentation, sync lock deprecation
45
+ - **2 new features** from ECC/dmux patterns: seedPaths overlay + structured handoff template
46
+ - **2 gap fills**: handoff parser + per-step seedPaths
47
+ - **36 skills reviewed**: origin fields, broken refs fixed, verify-skill.ts updated
48
+ - **1 bug fix**: adaptive-plan parser strips markdown code fences
49
+ - **1 regression fix**: mock mode NODE_ENV gate reverted
50
+ - **41 new tests** across 6 test files
51
+
52
+ ### Security Fixes
53
+
54
+ #### CRITICAL
55
+ 1. `orchestrate.ts`: Path traversal — planPath validated with `resolveContainedPath()`
56
+ 2. `sandbox.ts`: Prototype pollution — `Object.freeze` on prototypes, `globalThis`/`global` in FORBIDDEN_PATTERNS
57
+ 3. `dynamic-script-runner.ts`: `executeUnchecked` → private, `__test_executeUnchecked` test-only export
58
+
59
+ #### HIGH
60
+ 4. `safe-bash.ts`: allowPatterns validation rejects `/.*/` and permissive catch-all patterns
61
+ 5. `safe-bash-extension.ts`: Error message no longer suggests bypassing safe-bash
62
+ 6. `child-pi.ts`: Mock mode requires `PI_CREW_ALLOW_MOCK=1` (set in parent process only)
63
+
64
+ #### MEDIUM
65
+ 7. `worktree-manager.ts`: `logInternalError` warning when home directory hooks accepted
66
+ 8. `child-pi.ts`: SECURITY WARNING JSDoc on API key allow-list trade-off
67
+ 9. `event-log.ts`: Expanded deprecation notice on `withEventLogLockSync` blocking behavior
68
+
69
+ ### Features (ECC/dmux patterns)
70
+
71
+ - **seedPaths**: Overlay local/uncommitted files into worktrees via config (`worktree.seedPaths`) or per-step (`WorkflowStep.seedPaths`). Path traversal validation, dedup, recursive copy.
72
+ - **Structured Handoff Template**: `HANDOFF_TEMPLATE` constant + `parseHandoffFromOutput()` parser. Agents receive handoff format instructions automatically.
73
+
74
+ ### Skill Review
75
+ - All 36 skills: added `origin` YAML frontmatter field
76
+ - Fixed `widget-rendering` wrong file path
77
+ - Fixed `orchestration` + `detection-pipeline-design` broken cross-skill references
78
+ - Fixed 4 skills with wrong `source/pi-mono/` paths
79
+ - `verify-skill.ts` now validates `origin` field
80
+
81
+ ### Bug Fixes
82
+ - `adaptive-plan.ts`: `stripCodeFence()` strips markdown code fences inside ADAPTIVE_PLAN markers — fixes planner output parsing for non-frontier models
83
+ - Mock mode regression: reverted NODE_ENV gate, uses PI_CREW_ALLOW_MOCK only (child processes don't inherit NODE_ENV)
84
+
85
+ ### Stats
86
+ - Test suite: 2698 pass + 1 skip, 0 fail (was 2657 in v0.5.16; +41 net)
87
+ - TypeScript: 0 errors
88
+ - New test files: 6 (worktree-seed-paths, task-handoff-template, task-handoff-parser, adaptive-plan +3 safe-bash tests)
89
+ - Files touched: 50+
90
+ - Security issues fixed: 9 (3 CRITICAL + 3 HIGH + 3 MEDIUM)
91
+ - False positives verified: 2
92
+
3
93
  ## [0.5.16] — Rounds 22–31 Audit Fixes (2026-06-02)
4
94
 
5
95
  ### Highlights
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -48,9 +48,9 @@
48
48
  "check:lazy-imports": "node scripts/check-lazy-imports.mjs",
49
49
  "typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
50
50
  "test": "npm run test:unit && npm run test:integration",
51
- "test:unit": "NODE_ENV=test tsx --test --test-concurrency=4 --test-timeout=180000 --test-force-exit test/unit/*.test.ts",
51
+ "test:unit": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=180000 --test-force-exit test/unit/*.test.ts",
52
52
  "test:watch": "tsx --watch --test --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/*.test.ts",
53
- "test:integration": "NODE_ENV=test tsx --test --test-concurrency=1 --test-timeout=120000 test/integration/*.test.ts",
53
+ "test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=120000 test/integration/*.test.ts",
54
54
  "build:bundle": "node scripts/build-bundle.mjs",
55
55
  "bench": "node scripts/run-bench.mjs",
56
56
  "bench:check": "node scripts/bench-check.mjs",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: artifact-analysis-loop
3
3
  description: "Systematic artifact examination for code, files, and binaries."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "analyze this artifact"
6
7
  - "examine file"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: async-worker-recovery
3
3
  description: Background worker, heartbeat, stale-run, crash-recovery, and deadletter workflow. Use when debugging stuck/dead workers or changing async run reliability.
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "worker crashed"
6
7
  - "stale run"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: child-pi-spawning
3
3
  description: "Child Pi worker spawning, lifecycle callbacks, and failure modes."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "worker crashed"
6
7
  - "worker blink"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: context-artifact-hygiene
3
3
  description: "Use when constructing worker prompts, reading artifacts/logs, summarizing runs, compacting context, or handing work between agents."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "construct prompt"
6
7
  - "read artifact"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: delegation-patterns
3
3
  description: "Subagent/team delegation workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "delegate this"
6
7
  - "split this task"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: detection-pipeline-design
3
3
  description: "Design data pipelines for security monitoring and threat intelligence."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "build pipeline"
6
7
  - "design detection"
@@ -282,4 +283,4 @@ npx tsc --noEmit
282
283
  node --experimental-strip-types --test test/unit/detection-pipeline.test.ts
283
284
  ```
284
285
 
285
- *See also: `detection-signature-authoring` (in security-review) for detection rule patterns.*
286
+ *See also: `security-review` skill for detection rule patterns and signature authoring guidance.*
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: event-log-tracing
3
3
  description: "Structured event logging for worker lifecycle, live agents, crash recovery."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "event log"
6
7
  - "trace events"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: git-master
3
3
  description: "Commit and release hygiene for safe version-control work."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "commit this"
6
7
  - "tag release"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: hunting-investigation-loop
3
3
  description: "Active hypothesis-driven investigation and threat hunting."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "hunt for"
6
7
  - "find evidence of"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: incident-playbook-construction
3
3
  description: "Build structured incident response playbooks and runbooks."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "build playbook"
6
7
  - "create runbook"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: iterative-audit
3
3
  description: "Iterative multi-round codebase audit with diminishing-returns detection. Run 5-20+ rounds, each focusing on one specific area. Built from 19 rounds of dogfooding pi-crew on itself."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "audit this codebase"
6
7
  - "review everything"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: live-agent-lifecycle
3
3
  description: "Live agent registration, workspace isolation, termination, and eviction workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "register agent"
6
7
  - "terminate agent"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: mailbox-interactive
3
3
  description: "Interactive waiting-task and mailbox workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "respond to worker"
6
7
  - "nudge agent"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: model-routing-context
3
3
  description: Model routing, parent context, thinking level, and prompt construction workflow. Use when changing model fallback, child Pi args, inherited context, task prompts, or compact-read behavior.
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "change model"
6
7
  - "parent context"
@@ -15,7 +16,7 @@ Use this skill when working on model/context propagation.
15
16
 
16
17
  ## Source patterns distilled
17
18
 
18
- - Pi session context/model state: `source/pi-mono/packages/coding-agent/src/core/session-manager.ts`, `agent-session.ts`, compaction modules
19
+ - Pi session context/model state: `source/pi/packages/coding-agent/src/core/session-manager.ts`, `agent-session.ts`, compaction modules
19
20
  - pi-crew model and prompt code: `src/runtime/model-fallback.ts`, `src/runtime/pi-args.ts`, `src/runtime/task-runner/prompt-builder.ts`, `src/runtime/task-output-context.ts`, `src/extension/team-tool/context.ts`
20
21
 
21
22
  ## Rules
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: multi-perspective-review
3
3
  description: "Multi-perspective code review with simpler-alternative pass."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "review this"
6
7
  - "look at this"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: observability-reliability
3
3
  description: "Metrics, diagnostics, correlation, retry, deadletter, and recovery evidence workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "add metrics"
6
7
  - "diagnose failure"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: orchestration
3
3
  description: "Multi-phase orchestration for planners and executors."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "orchestrate this"
6
7
  - "coordinate tasks"
@@ -73,7 +74,7 @@ Maintain the original scope exactly. Không mở rộng scope vì "thấy thêm
73
74
  ### Step 3 — Dispatch phase
74
75
 
75
76
  - Launch all parallel subagents in one `team` call.
76
- - Each subagent receives a complete task packet (see `task-packet` skill).
77
+ - Each subagent receives a complete task packet (see `requirements-to-task-packet` skill).
77
78
  - Set explicit file ownership per worker — no two workers touch the same file.
78
79
  - Use `workspaceMode: 'worktree'` when parallel edits risk conflict.
79
80
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: ownership-session-security
3
3
  description: "Session ownership and authorization workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "cancel run"
6
7
  - "respond to task"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: pi-extension-lifecycle
3
3
  description: Pi extension lifecycle and registration patterns.
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "add extension"
6
7
  - "register tools"
@@ -15,8 +16,8 @@ Use this skill when working on Pi extension registration or lifecycle behavior.
15
16
 
16
17
  ## Source patterns distilled
17
18
 
18
- - Pi core: `source/pi-mono/packages/coding-agent/src/core/extensions/types.ts`, `loader.ts`, `runner.ts`
19
- - Pi examples: `source/pi-mono/packages/coding-agent/examples/extensions/`
19
+ - Pi core: `source/pi/packages/coding-agent/src/core/extensions/types.ts`, `loader.ts`, `runner.ts`
20
+ - Pi examples: `source/pi/packages/coding-agent/examples/extensions/`
20
21
  - pi-crew extension entry: `src/extension/register.ts`, `src/extension/registration/*.ts`
21
22
 
22
23
  ## Rules
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: post-mortem
3
3
  description: "Write engineering RCA record after bug is fixed."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "post-mortem"
6
7
  - "root cause"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: read-only-explorer
3
3
  description: "Read-only exploration and audit workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "explore code"
6
7
  - "audit source"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: requirements-to-task-packet
3
3
  description: "Use when a goal, issue, roadmap item, review finding, or user request must become actionable worker tasks."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "convert requirements"
6
7
  - "create task packet"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: resource-discovery-config
3
3
  description: "pi-crew resource and configuration discovery workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "discover agents"
6
7
  - "find teams"
@@ -15,7 +16,7 @@ Use this skill for pi-crew resource/config work.
15
16
 
16
17
  ## Source patterns distilled
17
18
 
18
- - Pi resource loader: `source/pi-mono/packages/coding-agent/src/core/resource-loader.ts`, extension `resources_discover` hook
19
+ - Pi resource loader: `source/pi/packages/coding-agent/src/core/resource-loader.ts`, extension `resources_discover` hook
19
20
  - pi-crew discovery: `src/agents/discover-agents.ts`, `src/teams/discover-teams.ts`, `src/workflows/discover-workflows.ts`
20
21
  - Config: `src/config/config.ts`, `src/schema/config-schema.ts`, `schema.json`, `docs/resource-formats.md`
21
22
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: runtime-state-reader
3
3
  description: Safe read-only navigation of pi-crew run state.
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "inspect manifest"
6
7
  - "read tasks"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: safe-bash
3
3
  description: "Safe shell-command workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "run this command"
6
7
  - "execute bash"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: scrutinize
3
3
  description: "Outsider-perspective review questioning intent before tracing code."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "scrutinize this"
6
7
  - "question this"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: secure-agent-orchestration-review
3
3
  description: "Use when reviewing delegation, skill loading, tool access, worker prompts, artifacts, runtime config, state, ownership, or subprocess execution."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "review delegation"
6
7
  - "check skill security"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: security-review
3
3
  description: "Security review patterns with audit and detection authoring."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "security review"
6
7
  - "vulnerability scan"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: state-mutation-locking
3
3
  description: "Durable state mutation and locking workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "modify manifest"
6
7
  - "update tasks"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: systematic-debugging
3
3
  description: "Four-phase debugging discipline with refuse gates."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "debug this"
6
7
  - "investigate"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: threat-hypothesis-framework
3
3
  description: "Structured investigation using testable hypotheses."
4
+ origin: distilled:anthropic-cybersecurity-skills
4
5
  triggers:
5
6
  - "hunt for"
6
7
  - "investigate"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: ui-render-performance
3
3
  description: "Non-blocking Pi TUI render workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "widget render"
6
7
  - "dashboard pane"
@@ -14,7 +15,7 @@ Use this skill for Pi/pi-crew TUI work.
14
15
 
15
16
  ## Source patterns distilled
16
17
 
17
- - Pi TUI is synchronous immediate-mode/string rendering: `source/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts`
18
+ - Pi TUI is synchronous immediate-mode/string rendering: `source/pi/packages/coding-agent/src/modes/interactive/interactive-mode.ts`
18
19
  - Pi extension examples use event-driven state updates, not render-time loading.
19
20
  - pi-crew UI: `src/extension/register.ts`, `src/ui/run-dashboard.ts`, `src/ui/run-snapshot-cache.ts`, `src/ui/crew-widget.ts`, `src/ui/powerbar-publisher.ts`, `src/ui/render-scheduler.ts`
20
21
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: verification-before-done
3
3
  description: "Evidence before claims."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "done"
6
7
  - "verify this"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: widget-rendering
3
3
  description: "Pi TUI crew widget data sources, display priority, and rendering performance."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "empty agent"
6
7
  - "ghost run"
@@ -259,7 +260,7 @@ If ANY answer is NO → Stop. Fix widget rendering issues before proceeding.
259
260
  - `src/runtime/crew-agent-records.ts` — readCrewAgents, agents.json
260
261
  - `src/runtime/process-status.ts` — hasStaleAsyncProcess, isDisplayActiveRun
261
262
  - `src/runtime/background-runner.ts` — active run filtering with async PID check
262
- - `src/runtime/active-run-registry.ts` — purgeStaleActiveRunIndex
263
+ - `src/state/active-run-registry.ts` — purgeStaleActiveRunIndex
263
264
 
264
265
  ---
265
266
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: workspace-isolation
3
3
  description: "Workspace isolation boundaries."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "workspace isolation"
6
7
  - "cross-workspace access"
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: worktree-isolation
3
3
  description: "Conflict-safe git worktree workflow."
4
+ origin: pi-crew
4
5
  triggers:
5
6
  - "create worktree"
6
7
  - "parallel workers"
@@ -81,6 +81,7 @@ export interface CrewWorktreeConfig {
81
81
  setupHook?: string;
82
82
  setupHookTimeoutMs?: number;
83
83
  linkNodeModules?: boolean;
84
+ seedPaths?: string[];
84
85
  }
85
86
 
86
87
  export interface CrewUiConfig {
@@ -15,6 +15,7 @@ import {
15
15
  parsePlanDocumentSimple,
16
16
  type OrchestratedStep,
17
17
  } from "../plan-orchestrate.ts";
18
+ import { resolveContainedPath } from "../../utils/safe-paths.ts";
18
19
 
19
20
  /**
20
21
  * Handle the orchestrate action.
@@ -38,10 +39,17 @@ export function handleOrchestrate(
38
39
  );
39
40
  }
40
41
 
41
- // Resolve relative paths against ctx.cwd
42
- const resolvedPath = path.isAbsolute(planPath)
43
- ? planPath
44
- : path.resolve(ctx.cwd, planPath);
42
+ // Resolve and validate path stays within ctx.cwd (path-traversal protection)
43
+ let resolvedPath: string;
44
+ try {
45
+ resolvedPath = resolveContainedPath(ctx.cwd, planPath);
46
+ } catch {
47
+ return result(
48
+ `planPath must be within project directory: ${planPath}`,
49
+ { action: "orchestrate", status: "error" },
50
+ true,
51
+ );
52
+ }
45
53
 
46
54
  if (!fs.existsSync(resolvedPath)) {
47
55
  return result(
@@ -44,11 +44,27 @@ export function slug(value: string): string {
44
44
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
45
45
  }
46
46
 
47
+ /** Strip surrounding markdown code fences if present. */
48
+ function stripCodeFence(raw: string): string {
49
+ let s = raw.trim();
50
+ // Remove opening fence: ```json or ```
51
+ if (s.startsWith("```")) {
52
+ const firstNewline = s.indexOf("\n");
53
+ if (firstNewline >= 0) s = s.slice(firstNewline + 1);
54
+ else s = s.slice(3); // edge case: ``` alone on one line
55
+ }
56
+ // Remove closing fence
57
+ if (s.endsWith("```")) {
58
+ s = s.slice(0, -3);
59
+ }
60
+ return s.trim();
61
+ }
62
+
47
63
  export function extractAdaptivePlanJson(text: string): string | undefined {
48
64
  const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
49
- if (markerMatch?.[1]) return markerMatch[1];
65
+ if (markerMatch?.[1]) return stripCodeFence(markerMatch[1]);
50
66
  const startIndex = text.indexOf("ADAPTIVE_PLAN_JSON_START");
51
- if (startIndex >= 0) return text.slice(startIndex + "ADAPTIVE_PLAN_JSON_START".length).trim();
67
+ if (startIndex >= 0) return stripCodeFence(text.slice(startIndex + "ADAPTIVE_PLAN_JSON_START".length));
52
68
  const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
53
69
  return fencedMatch?.[1];
54
70
  }
@@ -181,6 +181,16 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv): S
181
181
  // Bug #12 fix: essential env vars (PATH, HOME, etc.) are always preserved so child can find npm/node.
182
182
  const filteredEnv = sanitizeEnvSecrets(env, {
183
183
  allowList: [
184
+ /*
185
+ * SECURITY WARNING: All model provider API keys below are passed to EVERY child worker.
186
+ * If any child is compromised (e.g. via prompt injection), all listed keys are exposed.
187
+ * This is a deliberate trade-off: multi-provider setups require the child Pi process to
188
+ * authenticate with whichever provider the model routes to. Reducing keys per-child
189
+ * would break multi-provider functionality. Mitigations:
190
+ * - sanitizeEnvSecrets strips all env vars NOT on this list.
191
+ * - Do NOT add wildcards ("*_API_KEY") — only explicit, intended provider keys.
192
+ * - Consider per-task key scoping if the architecture allows it in the future.
193
+ */
184
194
  // Model provider API keys (explicit list — do NOT use wildcards)
185
195
  "MINIMAX_API_KEY",
186
196
  "MINIMAX_GROUP_ID",
@@ -405,14 +415,16 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
405
415
  if (depth.blocked) return { exitCode: 1, stdout: "", stderr: `pi-crew depth guard blocked child worker: depth ${depth.depth} >= max ${depth.maxDepth}` };
406
416
  const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
407
417
  if (mock) {
408
- // SECURITY: Log mock mode activation prominently for audit trail
409
- console.warn(`[⚠️ PI_CREW_MOCK_MODE] Mock mode active: ${mock} NOT running real agents!`);
410
- // SECURITY FIX: Require PI_CREW_ALLOW_MOCK alongside PI_TEAMS_MOCK_CHILD_PI
418
+ // SECURITY: Require explicit PI_CREW_ALLOW_MOCK=1 to activate mock mode.
419
+ // PI_CREW_ALLOW_MOCK must be set in the parent process env (not by child hooks)
420
+ // since sanitizeEnvSecrets only passes PI_CREW_* vars from the parent.
421
+ // Setup hooks cannot inject PI_CREW_ALLOW_MOCK into the parent's env.
411
422
  const allowMock = process.env.PI_CREW_ALLOW_MOCK === "1" || process.env.PI_CREW_ALLOW_MOCK === "true";
412
423
  if (!allowMock) {
413
- console.error(`[🚨 PI_CREW_MOCK_MODE] SECURITY: PI_TEAMS_MOCK_CHILD_PI is set but PI_CREW_ALLOW_MOCK is not "1". Ignoring mock request for safety.`);
414
- return { exitCode: 1, stdout: "", stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1 alongside PI_TEAMS_MOCK_CHILD_PI" };
424
+ return { exitCode: 1, stdout: "", stderr: "Mock mode requires PI_CREW_ALLOW_MOCK=1" };
415
425
  }
426
+ // SECURITY: Log mock mode activation prominently for audit trail
427
+ console.warn(`Mock mode active: ${mock} — NOT running real agents!`);
416
428
  if (mock === "success") {
417
429
  const stdout = `[MOCK] Success for ${input.agent.name}\n`;
418
430
  observeStdoutChunk(input, stdout);
@@ -444,8 +444,9 @@ export class DynamicScriptRunner {
444
444
  /**
445
445
  * Execute a script without validation (assumes pre-validated).
446
446
  * Use with caution - prefer execute() for untrusted scripts.
447
+ * @internal TEST ONLY — do not use in production code
447
448
  */
448
- executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
+ private executeUnchecked(code: string, timeout?: number): ScriptExecutionResult {
449
450
  const startTime = Date.now();
450
451
 
451
452
  try {
@@ -480,3 +481,15 @@ export class DynamicScriptRunner {
480
481
  export function createScriptRunner(options?: DynamicScriptOptions): DynamicScriptRunner {
481
482
  return new DynamicScriptRunner(options);
482
483
  }
484
+
485
+ /**
486
+ * @internal TEST ONLY — do not use in production code.
487
+ * Exposes DynamicScriptRunner.executeUnchecked for unit testing.
488
+ */
489
+ export function __test_executeUnchecked(
490
+ runner: DynamicScriptRunner,
491
+ code: string,
492
+ timeout?: number,
493
+ ): ScriptExecutionResult {
494
+ return (runner as unknown as { executeUnchecked: (code: string, timeout?: number) => ScriptExecutionResult }).executeUnchecked(code, timeout);
495
+ }
@@ -18,6 +18,9 @@ const FORBIDDEN_PATTERNS = [
18
18
  /__dirname/, // __dirname reference
19
19
  /__filename/, // __filename reference
20
20
  /\bdefine\s*\(/, // AMD define
21
+ // Global escape vectors
22
+ /\bglobalThis\b/, // globalThis reference
23
+ /\bglobal\b/, // global reference (Node.js)
21
24
  ] as const;
22
25
 
23
26
  /**
@@ -165,7 +168,35 @@ export class WorkflowSandbox {
165
168
  Uint8Array: Uint8Array,
166
169
  };
167
170
 
168
- return vm.createContext(contextGlobals);
171
+ // Freeze the context object itself to prevent sandbox code from
172
+ // adding/removing globals.
173
+ Object.freeze(contextGlobals);
174
+
175
+ const ctx = vm.createContext(contextGlobals);
176
+
177
+ // Freeze prototypes INSIDE the VM context to prevent sandboxed code
178
+ // from polluting Object.prototype or Array.prototype.
179
+ //
180
+ // SECURITY TRADE-OFF: vm.createContext shares host prototypes, so
181
+ // freezing inside the context also freezes them for the host process.
182
+ // This is acceptable because:
183
+ // 1. Pi-crew extensions should not modify built-in prototypes
184
+ // 2. The freeze is idempotent (safe to call multiple times)
185
+ // 3. In test environments, we skip this to allow test frameworks
186
+ // that extend prototypes (e.g., Sinon, should.js)
187
+ if (process.env.NODE_ENV !== "test") {
188
+ try {
189
+ vm.runInContext(
190
+ "Object.freeze(Object.prototype); Object.freeze(Array.prototype);",
191
+ ctx,
192
+ { filename: "sandbox-init.js", timeout: 1000 },
193
+ );
194
+ } catch {
195
+ // Already frozen — idempotent, safe to ignore
196
+ }
197
+ }
198
+
199
+ return ctx;
169
200
  }
170
201
 
171
202
  /**
@@ -128,6 +128,130 @@ export function validateTaskPacket(packet: TaskPacket): TaskPacketValidationResu
128
128
  return { valid: errors.length === 0, errors };
129
129
  }
130
130
 
131
+ /**
132
+ * Structured handoff template for task completion reports.
133
+ * Distilled from ECC dmux-workflows pattern — workers use this format
134
+ * so verifiers and downstream consumers can parse output predictably.
135
+ */
136
+ export const HANDOFF_TEMPLATE = [
137
+ "## Handoff",
138
+ "",
139
+ "### Summary",
140
+ "<!-- 2-3 sentences describing what was done -->",
141
+ "",
142
+ "### Files Changed",
143
+ "<!-- List each file changed with brief description -->",
144
+ "<!-- - path/to/file.ts: description -->",
145
+ "",
146
+ "### Tests / Verification",
147
+ "<!-- What tests pass? What was manually verified? -->",
148
+ "",
149
+ "### Follow-ups",
150
+ "<!-- Any remaining issues or next steps -->",
151
+ ].join("\n");
152
+
153
+ export interface ParsedHandoff {
154
+ summary: string[];
155
+ filesChanged: string[];
156
+ tests: string[];
157
+ followups: string[];
158
+ }
159
+
160
+ /**
161
+ * Extract text between a ### heading and the next ### heading or end of text.
162
+ */
163
+ function extractSection(content: string, heading: string): string {
164
+ const lines = content.split("\n");
165
+ const headingMarker = `### ${heading}`;
166
+ const startIndex = lines.findIndex((line) => line.trim() === headingMarker);
167
+ if (startIndex === -1) return "";
168
+
169
+ const collected: string[] = [];
170
+ for (let i = startIndex + 1; i < lines.length; i++) {
171
+ const trimmed = lines[i].trim();
172
+ if (trimmed.startsWith("### ") || trimmed.startsWith("## ")) break;
173
+ // Stop at paragraph text (non-bullet, non-comment, non-empty) that follows
174
+ // a blank line — signals end of subsection content.
175
+ if (
176
+ trimmed.length > 0 &&
177
+ !trimmed.startsWith("- ") &&
178
+ !trimmed.startsWith("<!--") &&
179
+ i > startIndex + 1 &&
180
+ lines[i - 1].trim() === "" &&
181
+ collected.some((l) => l.trim().length > 0)
182
+ ) {
183
+ break;
184
+ }
185
+ collected.push(lines[i]);
186
+ }
187
+
188
+ return collected.join("\n").trim();
189
+ }
190
+
191
+ /**
192
+ * Parse bullet list items from a section, stripping leading "- " and backtick wrapping.
193
+ */
194
+ function parseBullets(section: string): string[] {
195
+ if (!section) return [];
196
+ return section
197
+ .split("\n")
198
+ .map((line) => line.trim())
199
+ .filter((line) => line.startsWith("- "))
200
+ .map((line) => {
201
+ let item = line.replace(/^- /, "").trim();
202
+ // Strip surrounding backticks
203
+ if (item.startsWith("`") && item.endsWith("`") && item.length >= 2) {
204
+ item = item.slice(1, -1);
205
+ }
206
+ return item;
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Parse a handoff section that may contain bullets AND free-text paragraphs.
212
+ * Returns all non-empty lines as individual items (bullets get their marker stripped).
213
+ */
214
+ function parseMixedContent(section: string): string[] {
215
+ if (!section) return [];
216
+ return section
217
+ .split("\n")
218
+ .map((line) => line.trim())
219
+ .filter((line) => line.length > 0 && !line.startsWith("<!--")) // skip HTML comments
220
+ .map((line) => {
221
+ if (line.startsWith("- ")) return line.slice(2).trim();
222
+ return line;
223
+ })
224
+ .map((item) => {
225
+ // Strip surrounding backticks
226
+ if (item.startsWith("`") && item.endsWith("`") && item.length >= 2) {
227
+ return item.slice(1, -1);
228
+ }
229
+ return item;
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Parse structured handoff data from agent output text.
235
+ * Looks for the "## Handoff" heading and extracts subsections.
236
+ * Returns empty arrays for sections not found.
237
+ */
238
+ export function parseHandoffFromOutput(output: string): ParsedHandoff {
239
+ if (!output || typeof output !== "string") {
240
+ return { summary: [], filesChanged: [], tests: [], followups: [] };
241
+ }
242
+
243
+ // Find the handoff section — look for ## Handoff
244
+ const handoffIndex = output.indexOf("## Handoff");
245
+ const content = handoffIndex >= 0 ? output.slice(handoffIndex) : output;
246
+
247
+ return {
248
+ summary: parseMixedContent(extractSection(content, "Summary")),
249
+ filesChanged: parseMixedContent(extractSection(content, "Files Changed")),
250
+ tests: parseMixedContent(extractSection(content, "Tests / Verification")),
251
+ followups: parseMixedContent(extractSection(content, "Follow-ups")),
252
+ };
253
+ }
254
+
131
255
  export function renderTaskPacket(packet: TaskPacket): string {
132
256
  return [
133
257
  "# Task Packet",
@@ -3,7 +3,7 @@ import type { TeamRunManifest, TeamTaskState, TaskOutputSchema } from "../../sta
3
3
  import type { WorkflowStep } from "../../workflows/workflow-config.ts";
4
4
  import { buildMemoryBlock } from "../agent-memory.ts";
5
5
  import { permissionForRole } from "../role-permission.ts";
6
- import { renderTaskPacket } from "../task-packet.ts";
6
+ import { renderTaskPacket, HANDOFF_TEMPLATE } from "../task-packet.ts";
7
7
  import { buildWorkspaceTree } from "../workspace-tree.ts";
8
8
 
9
9
  /**
@@ -132,6 +132,9 @@ export async function renderTaskPrompt(manifest: TeamRunManifest, step: Workflow
132
132
  task.taskPacket?.outputSchema ? renderOutputSchemaBlock(task.taskPacket.outputSchema) : "",
133
133
  "Task:",
134
134
  step.task.replaceAll("{goal}", manifest.goal),
135
+ "",
136
+ "When your task is complete, structure your final output using this handoff template:",
137
+ HANDOFF_TEMPLATE,
135
138
  ].join("\n");
136
139
 
137
140
  const full = [stablePrefix, "", dynamicSuffix].join("\n");
@@ -156,7 +156,7 @@ export async function runTeamTask(
156
156
  let streamBridge: ReturnType<typeof registerStreamBridge> | undefined;
157
157
  try {
158
158
  streamBridge = registerStreamBridge(manifest.runId);
159
- const workspace = prepareTaskWorkspace(manifest, input.task);
159
+ const workspace = prepareTaskWorkspace(manifest, input.task, input.step.seedPaths);
160
160
  const worktree =
161
161
  workspace.worktreePath && workspace.branch
162
162
  ? {
@@ -56,6 +56,7 @@ export const PiTeamsWorktreeConfigSchema = Type.Object({
56
56
  setupHook: Type.Optional(Type.String({ minLength: 1 })),
57
57
  setupHookTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
58
58
  linkNodeModules: Type.Optional(Type.Boolean()),
59
+ seedPaths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
59
60
  }, { additionalProperties: false });
60
61
 
61
62
  export const AgentOverrideSchema = Type.Object({
@@ -66,6 +66,13 @@ let appendCounter = 0;
66
66
  *
67
67
  * @deprecated Prefer `appendEventAsync()` for callers in async contexts. The sync lock
68
68
  * uses `sleepSync` which blocks the event loop and prevents AbortSignal handlers from firing.
69
+ *
70
+ * SECURITY WARNING: This function uses `sleepSync` in its lock-acquire retry loop, which
71
+ * blocks the Node.js event loop for up to 120s. During that time, AbortSignal handlers
72
+ * cannot fire, SIGTERM handlers are delayed, and the process appears unresponsive to
73
+ * orchestrator health checks. Known callers include `appendEvent` (sync path),
74
+ * `flushOneEventLogBuffer`, and `state/mailbox.ts`. Prefer the async alternative
75
+ * (`appendEventAsync`) for all new code.
69
76
  */
70
77
  export function withEventLogLockSync<T>(eventsPath: string, fn: () => T): T {
71
78
  // Ensure parent directory exists before attempting lock
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Safe Bash Extension for pi-crew
3
- * Wraps the built-in bash tool with dangerous command blocking
4
- *
3
+ * Wraps the built-in bash tool with dangerous command blocking.
4
+ *
5
+ * Delegates pattern matching to the core `safe-bash.ts` module which uses
6
+ * linear-time string scanning (no ReDoS-vulnerable regex).
7
+ *
5
8
  * Usage:
6
9
  * 1. Enable in config: { "tools": { "bash": { "safeMode": true } } }
7
10
  * 2. Or use via agent config: { "extensions": ["path/to/safe-bash-extension.ts"] }
@@ -11,51 +14,7 @@
11
14
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
15
  import { createBashTool } from "@earendil-works/pi-coding-agent";
13
16
  import { Type } from "@sinclair/typebox";
14
-
15
- // Dangerous command patterns to block
16
- const DANGEROUS_PATTERNS = [
17
- // rm -rf on root or home
18
- /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
19
- /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
20
- // Privilege escalation
21
- /\bsudo\b/,
22
- /\bsu\s+root\b/,
23
- // Filesystem destruction
24
- /\bmkfs\b/,
25
- /\bdd\s+if=/,
26
- // Fork bomb
27
- /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/,
28
- // Device writing
29
- />\s*\/dev\/[sh]d[a-z]/,
30
- /\bchmod\s+(-[a-zA-Z]+\s+)?777\s+\//,
31
- /\bchown\s+(-[a-zA-Z]+\s+)?root/,
32
- // Pipe to shell (download and execute)
33
- /\bcurl\s.*\|\s*(ba)?sh/i,
34
- /\bwget\s.*\|\s*(ba)?sh/i,
35
- // System shutdown/reboot
36
- /\bshutdown\b/,
37
- /\breboot\b/,
38
- /\binit\s+0\b/,
39
- // Kill critical processes
40
- /\bkill\s+-9\s+1\b/,
41
- /\bkillall\b/,
42
- // Encoded commands
43
- /\|\s*base64\s+-d/,
44
- // Network to shell
45
- /\bbash\s+-i\s+>\s*\&/,
46
- // /etc/passwd manipulation
47
- /\becho\s+.*>\s*\/etc\/passwd/,
48
- ];
49
-
50
- function isDangerous(command: string): string | null {
51
- const normalized = command.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim();
52
- for (const pattern of DANGEROUS_PATTERNS) {
53
- if (pattern.test(normalized)) {
54
- return `Command blocked: matches dangerous pattern \`${pattern}\``;
55
- }
56
- }
57
- return null;
58
- }
17
+ import { isDangerous } from "./safe-bash.ts";
59
18
 
60
19
  export default function safeBashExtension(pi: ExtensionAPI): void {
61
20
  const cwd = process.cwd();
@@ -84,7 +43,7 @@ export default function safeBashExtension(pi: ExtensionAPI): void {
84
43
  content: [
85
44
  {
86
45
  type: "text" as const,
87
- text: `🚫 ${danger}\n\nIf you need to run this command, use the regular 'bash' tool instead, but be careful!`,
46
+ text: `🚫 ${danger}\n\nCommand blocked by safety policy. If this is a false positive, ask the user for confirmation or use force: true with explicit user approval.`,
88
47
  },
89
48
  ],
90
49
  };
@@ -93,4 +52,4 @@ export default function safeBashExtension(pi: ExtensionAPI): void {
93
52
  return bashTool.execute(toolCallId, params, signal, onUpdate);
94
53
  },
95
54
  });
96
- }
55
+ }
@@ -4,6 +4,8 @@
4
4
  * Uses linear-time scanning to prevent ReDoS attacks
5
5
  */
6
6
 
7
+ import { logInternalError } from "../utils/internal-error.ts";
8
+
7
9
 
8
10
  // Backward-compatible pattern array (kept for getPatterns API)
9
11
  // IMPORTANT: Line 8 (rm pattern with nested quantifiers) has been replaced
@@ -162,6 +164,14 @@ export function isDangerous(command: string, options: SafeBashOptions = {}): str
162
164
 
163
165
  if (!enabled) return null;
164
166
 
167
+ // Reject overly permissive allowPatterns that would bypass all safety
168
+ for (const pattern of allowPatterns) {
169
+ if (pattern.source === ".*" || (pattern.test("") && pattern.test("rm -rf /"))) {
170
+ logInternalError("safe-bash.permissive-allow-pattern", new Error(`allowPattern rejects nothing: ${pattern}`));
171
+ throw new Error(`Overly permissive allowPattern rejected: ${pattern}. Use specific patterns only.`);
172
+ }
173
+ }
174
+
165
175
  // Normalize: remove line continuations, collapse whitespace
166
176
  const normalized = command.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim();
167
177
 
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execSync } from "node:child_process";
2
+ import { execFileSync } from "node:child_process";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
 
@@ -54,7 +54,7 @@ function extractRepoName(remoteUrl: string): string | null {
54
54
  */
55
55
  function tryGitRemote(cwd: string): string | null {
56
56
  try {
57
- const remoteUrl = execSync("git remote get-url origin", {
57
+ const remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
58
58
  cwd,
59
59
  encoding: "utf-8",
60
60
  stdio: ["pipe", "pipe", "ignore"],
@@ -79,7 +79,7 @@ function tryGitRemote(cwd: string): string | null {
79
79
  */
80
80
  function tryGitToplevel(cwd: string): string | null {
81
81
  try {
82
- const toplevel = execSync("git rev-parse --show-toplevel", {
82
+ const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
83
83
  cwd,
84
84
  encoding: "utf-8",
85
85
  stdio: ["pipe", "pipe", "ignore"],
@@ -14,6 +14,9 @@ export interface WorkflowStep {
14
14
  progress?: boolean;
15
15
  worktree?: boolean;
16
16
  verify?: boolean;
17
+ /** Per-step files to overlay into the worktree (in addition to global worktree.seedPaths).
18
+ * Useful when only certain steps need access to local drafts or scripts. */
19
+ seedPaths?: string[];
17
20
  }
18
21
 
19
22
  export interface WorkflowConfig {
@@ -116,6 +116,11 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
116
116
  logInternalError("worktree.setupHook.rejected", new Error("hook path not allowed: " + rawHookPath), `cwd=${manifest.cwd}`);
117
117
  return [];
118
118
  }
119
+ // SECURITY WARNING: Home directory hooks (~/.pi/hooks/) are user-writable and not project-scoped.
120
+ // A rogue npm postinstall script could place malicious hooks there. Log for visibility.
121
+ if (path.isAbsolute(rawHookPath)) {
122
+ logInternalError("worktree.setupHook.homeHook", new Error("Home directory hook used — ensure ~/.pi/hooks/ is trusted"), `hookPath=${rawHookPath}`);
123
+ }
119
124
  const hookPath = path.isAbsolute(rawHookPath) ? rawHookPath : path.resolve(repoRoot, rawHookPath);
120
125
  // SECURITY: Verify the resolved hook path is contained within the real repoRoot.
121
126
  // This prevents symlink-based escape where repoRoot is a symlink.
@@ -201,7 +206,64 @@ function pruneStaleWorktrees(repoRoot: string): void {
201
206
  catch { /* best-effort */ }
202
207
  }
203
208
 
204
- export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState): PreparedTaskWorkspace {
209
+ /**
210
+ * Normalize and validate seed paths — ensure all paths stay within repoRoot.
211
+ * Rejects path traversal (../) and absolute paths.
212
+ */
213
+ export function normalizeSeedPaths(seedPaths: string[], repoRoot: string): string[] {
214
+ const resolvedRepoRoot = path.resolve(repoRoot);
215
+ const entries = Array.isArray(seedPaths) ? seedPaths : [];
216
+ const seen = new Set<string>();
217
+ const normalized: string[] = [];
218
+
219
+ for (const entry of entries) {
220
+ if (typeof entry !== "string" || entry.trim().length === 0) continue;
221
+
222
+ const absolutePath = path.resolve(resolvedRepoRoot, entry);
223
+ const relativePath = path.relative(resolvedRepoRoot, absolutePath);
224
+
225
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
226
+ throw new Error(`seedPaths entries must stay inside repoRoot: ${entry}`);
227
+ }
228
+
229
+ const normalizedPath = relativePath.split(path.sep).join("/");
230
+ if (seen.has(normalizedPath)) continue;
231
+ seen.add(normalizedPath);
232
+ normalized.push(normalizedPath);
233
+ }
234
+
235
+ return normalized;
236
+ }
237
+
238
+ /**
239
+ * Overlay seed paths from repoRoot into worktreePath.
240
+ * Copies files and directories, creating parent dirs as needed.
241
+ * Skips non-existent sources with logInternalError (non-fatal).
242
+ */
243
+ export function overlaySeedPaths(repoRoot: string, worktreePath: string, seedPaths: string[]): void {
244
+ const normalized = normalizeSeedPaths(seedPaths, repoRoot);
245
+
246
+ for (const seedPath of normalized) {
247
+ const sourcePath = path.join(repoRoot, seedPath);
248
+ const destinationPath = path.join(worktreePath, seedPath);
249
+
250
+ if (!fs.existsSync(sourcePath)) {
251
+ logInternalError("worktree.seedPaths.missing", new Error(`Seed path does not exist: ${seedPath}`));
252
+ continue;
253
+ }
254
+
255
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
256
+ fs.rmSync(destinationPath, { force: true, recursive: true });
257
+ fs.cpSync(sourcePath, destinationPath, {
258
+ dereference: false,
259
+ force: true,
260
+ preserveTimestamps: true,
261
+ recursive: true,
262
+ });
263
+ }
264
+ }
265
+
266
+ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState, stepSeedPaths?: string[]): PreparedTaskWorkspace {
205
267
  if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
206
268
  const repoRoot = findGitRoot(manifest.cwd);
207
269
  const loadedConfig = loadConfig(manifest.cwd);
@@ -220,6 +282,12 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
220
282
  if (currentBranch !== branch) {
221
283
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
222
284
  }
285
+ // Overlay seed paths from config + step-level seedPaths (reused worktree)
286
+ const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
287
+ const mergedReused = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
288
+ if (mergedReused.length > 0) {
289
+ overlaySeedPaths(repoRoot, worktreePath, mergedReused);
290
+ }
223
291
  return { cwd: worktreePath, worktreePath, branch, reused: true };
224
292
  }
225
293
  pruneStaleWorktrees(repoRoot);
@@ -242,6 +310,12 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
242
310
  }
243
311
  const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
244
312
  const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
313
+ // Overlay seed paths from config + step-level seedPaths
314
+ const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
315
+ const merged = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
316
+ if (merged.length > 0) {
317
+ overlaySeedPaths(repoRoot, worktreePath, merged);
318
+ }
245
319
  return { cwd: worktreePath, worktreePath, branch, reused: false, nodeModulesLinked, syntheticPaths };
246
320
  }
247
321