pi-crew 0.9.44 → 0.9.47

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 (65) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +38 -3
  3. package/dist/build-meta.json +349 -203
  4. package/dist/index.mjs +2229 -2968
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +77 -0
  7. package/docs/decisions/2026-07-21-broker-windows-perms.md +91 -0
  8. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +99 -0
  9. package/docs/decisions/README.md +3 -0
  10. package/docs/publishing.md +26 -0
  11. package/package.json +3 -1
  12. package/scripts/build-bundle.mjs +7 -0
  13. package/scripts/postinstall.mjs +35 -1
  14. package/scripts/pty_probe.py +174 -0
  15. package/skills/real-test-pi-crew/SKILL.md +659 -0
  16. package/src/agents/discover-agents.ts +1 -1
  17. package/src/config/config.ts +42 -1
  18. package/src/config/defaults.ts +45 -1
  19. package/src/config/types.ts +19 -0
  20. package/src/extension/register.ts +6 -1
  21. package/src/extension/registration/context-builder.ts +4 -0
  22. package/src/extension/registration/lifecycle-handlers.ts +200 -6
  23. package/src/extension/registration/registration-types.ts +9 -0
  24. package/src/extension/registration/subagent-manager-setup.ts +178 -59
  25. package/src/extension/run-import.ts +21 -1
  26. package/src/extension/team-tool/api.ts +4 -2
  27. package/src/prompt/prompt-runtime.ts +108 -0
  28. package/src/runtime/async-runner.ts +9 -1
  29. package/src/runtime/broker-issuer.ts +37 -0
  30. package/src/runtime/child-pi-spawn.ts +53 -0
  31. package/src/runtime/child-pi.ts +42 -11
  32. package/src/runtime/crew-broker-child.ts +88 -0
  33. package/src/runtime/crew-broker-client.ts +673 -0
  34. package/src/runtime/crew-broker-tokens.ts +84 -0
  35. package/src/runtime/crew-broker.ts +1276 -0
  36. package/src/runtime/dynamic-workflow-context.ts +7 -3
  37. package/src/runtime/dynamic-workflow-runner.ts +1 -1
  38. package/src/runtime/manifest-cache.ts +30 -0
  39. package/src/runtime/plan-templates.ts +8 -6
  40. package/src/runtime/resilient-edit.ts +16 -15
  41. package/src/runtime/role-permission.ts +27 -2
  42. package/src/runtime/run-coalesced-task-group.ts +72 -15
  43. package/src/runtime/task-packet.ts +1 -1
  44. package/src/schema/config-schema.ts +14 -0
  45. package/src/state/event-log.ts +88 -34
  46. package/src/state/locks.ts +53 -0
  47. package/src/state/mailbox.ts +208 -4
  48. package/src/state/run-metrics.ts +40 -12
  49. package/src/ui/key-utils.ts +42 -0
  50. package/src/ui/keybinding-map.ts +29 -3
  51. package/src/ui/live-run-sidebar.ts +1 -9
  52. package/src/ui/run-dashboard.ts +29 -9
  53. package/src/ui/settings-overlay.ts +42 -22
  54. package/src/utils/incremental-reader.ts +105 -0
  55. package/src/utils/ndjson.ts +115 -0
  56. package/src/utils/session-utils.ts +30 -0
  57. package/src/utils/socket-path.ts +127 -0
  58. package/src/utils/visual.ts +27 -91
  59. package/workflows/default.workflow.md +1 -1
  60. package/workflows/fast-fix.workflow.md +1 -1
  61. package/workflows/plan-execute.workflow.md +1 -1
  62. package/workflows/review.workflow.md +1 -1
  63. package/src/runtime/auto-resume.ts +0 -100
  64. package/src/runtime/notebook-helpers.ts +0 -88
  65. package/src/runtime/orphan-sentinel.ts +0 -7
@@ -0,0 +1,77 @@
1
+ # Decision: Phase 4 — Do not flip broker.enabled default to true
2
+
3
+ **Date:** 2026-07-21
4
+ **Status:** ⛔ SUPERSEDED on 2026-07-22 by `2026-07-22-broker-phase4-gated-on.md` (default flipped ON).
5
+ **Original status:** accepted (broker stays off-by-default)
6
+ **Scope:** Inter-pi broker default-off kill switch (broker.enabled, PI_CREW_BROKER env)
7
+ **Context:** Plan §7 Phase 4 + §Security invariant #11
8
+
9
+ > **Superseded by** [`2026-07-22-broker-phase4-gated-on.md`](2026-07-22-broker-phase4-gated-on.md).
10
+ > The Phase 4 default-on flip has shipped. This doc is preserved as the
11
+ > historical record of the conditions that gated the flip and the
12
+ > rationale for keeping default-off at v0.9.46.
13
+
14
+ ## Context
15
+
16
+ The inter-pi broker is feature-complete as of v0.9.46 (Phase 0 + 1 + 2 + 3):
17
+
18
+ - **Phase 0** — foundation (broker, client, deps, tokens), root-only gate, feature flag, default OFF.
19
+ - **Phase 1.1–1.2** — `msg.send` / `msg.inbox` (durable mailbox writes + paginated read).
20
+ - **Phase 1.3** — post-append mailbox observer (live fanout for connected recipients).
21
+ - **Phase 1.4** — disconnect fallback (already in client.ts via onClose / close()).
22
+ - **Phase 1.5** — `events.since` (durable event log replay, seq-deduped).
23
+ - **Phase 1.6** — E2E integration test (5 tests, all pass 3/3 runs).
24
+ - **Phase 1.7** — bench baseline recorded.
25
+ - **Phase 1.8** — bundle rebuilt.
26
+ - **Phase 2** — `events.subscribe` (live event stream via runEventBus.onWithReplay).
27
+ - **Phase 2** — `task.waitStatus` (bounded poll, properly recursive).
28
+ - **Phase 3** — `steer.push` (durable write to target task's mailbox, kind=steer, priority=urgent).
29
+ - **Phase 3** — `escalate` (durable write to sender's taskId, kind=follow-up).
30
+
31
+ Tests: 62 broker unit + 8 phase 2-3 integration + 5 msg integration = 75 tests, all pass 3/3 runs under `--test-force-exit` and `PI_CREW_BROKER=0`. Typecheck clean. Bundle rebuilt. The broker is correct, secure, and proven.
32
+
33
+ ## Decision
34
+
35
+ **Keep `broker.enabled: false` as the default.** Do not flip to `true` in this release.
36
+
37
+ The opt-in path remains:
38
+ - `broker.enabled: true` in `~/.pi/agent/extensions/pi-crew/config.json` or `pi-crew.json`
39
+ - `PI_CREW_BROKER=1` env override (beats config=false)
40
+
41
+ The opt-out path remains:
42
+ - `PI_CREW_BROKER=0` env override (beats config=true)
43
+ - `broker.enabled: false` in config
44
+
45
+ ## Rationale
46
+
47
+ 1. **Soak testing requires multi-OS CI + extended runtime.** Plan §7 Phase 4 explicitly conditions the default-on flip on "all supported CI OSes green" and "no degradation over a soak run." We have:
48
+ - Local Linux dev environment: passing.
49
+ - No macOS or Windows CI run.
50
+ - No 24-hour+ soak run.
51
+ The flip is premature without these signals.
52
+
53
+ 2. **Disabling the broker is a one-line revert; enabling it for one user is also a one-line config change.** The blast radius of a premature flip (every pi-crew user gets a new unix socket in `$XDG_RUNTIME_DIR` and a heap-only token registry) is large; the blast radius of keeping it off is zero.
54
+
55
+ 3. **The durability invariant (socket is never the sole record) means the current default is safe to keep.** Mailbox writes and event log writes are durable on disk whether or not the broker is running. The broker is a latency accelerator for cross-process messaging. Existing file-based pathways (`delivery.json` polling, `PI_CREW_STEERING_FILE` polling) remain authoritative.
56
+
57
+ 4. **Security review confirmed: flag-off path is leak-free.** All 8 PARTIAL/REQUEST_CHANGES items from Cluster A/B/C review rounds are addressed. Disabled-path tests (60 unit tests) pass with `PI_CREW_BROKER=0`. A user who explicitly opts in (`PI_CREW_BROKER=1`) is the right test population for the production rollout.
58
+
59
+ ## Conditions for re-opening this decision
60
+
61
+ Re-evaluate the default-on flip when ALL of the following are green:
62
+
63
+ - [ ] CI runs green on Linux + macOS + Windows for at least one release cycle.
64
+ - [ ] A 24-hour+ soak run shows no degradation (memory, connection count, token registry, socket files).
65
+ - [ ] At least 3 users have explicitly opted in (`PI_CREW_BROKER=1`) and reported no issues.
66
+ - [ ] The Windows residual risk (decision doc `2026-07-21-broker-windows-perms.md`) is acknowledged as accepted or mitigated.
67
+ - [ ] Bundle size impact of enabling the broker in the default config has been measured (current bundle: 2.78 MB; the broker code is ~80 KB but is already in the bundle).
68
+
69
+ Until then, the default stays `enabled: false`. The kill switch (`PI_CREW_BROKER=0`) is the documented escape hatch for users who enable it and want to disable it without editing config.
70
+
71
+ ## References
72
+
73
+ - Plan: `reports/inter-pi-broker-impl-plan-2026-07-21.md` §7 Phase 4 + §Security #11.
74
+ - Spec: `reports/inter-pi-broker-spec-2026-07-21.md` §3.3 (kill switch contract).
75
+ - Review rounds: Cluster A/B/C reports (see `.crew/artifacts/team_20260721*`).
76
+ - Test count: 75 tests, 3/3 runs, `--test-force-exit` and `PI_CREW_BROKER=0` (disabled-path proof).
77
+ - Bundle: `dist/index.mjs` 2.78 MB (built 2026-07-21).
@@ -0,0 +1,91 @@
1
+ # Decision: Windows broker named-pipe permissions
2
+
3
+ **Date:** 2026-07-21
4
+ **Status:** accepted (Phase 0)
5
+ **Context:** Plan §Q3 of `reports/inter-pi-broker-impl-plan-2026-07-21.md`
6
+
7
+ ## Context
8
+
9
+ The Phase 0 inter-pi broker uses a local-only socket transport: a Unix
10
+ domain socket on POSIX (`${XDG_RUNTIME_DIR:-/tmp}/pi-crew-<hash8>.sock`)
11
+ and a Windows named pipe (`\\.\pipe\pi-crew-broker-<hash8>`). On POSIX,
12
+ the runtime directory is mode `0700` and the socket is mode `0600`,
13
+ matching herdr's local-socket convention. Windows has no POSIX-equivalent
14
+ filesystem permission contract for named pipes; the closest the OS offers
15
+ is the `CreateNamedPipe` access-mask flags.
16
+
17
+ ## Decision
18
+
19
+ We treat the **per-run random token** as the canonical authentication
20
+ boundary on every platform, and complement it on Windows with the named
21
+ pipe options Node exposes (`readableAll: false`, `writableAll: false`)
22
+ where supported by the underlying API. POSIX retains its stronger
23
+ 0600/0700 contract as defense-in-depth.
24
+
25
+ ## Rationale
26
+
27
+ 1. The token is 128-bit-class randomness (`crypto.randomUUID()`), issued
28
+ per-run by the parent's `CrewBroker.issueRunToken` and stored only in
29
+ the parent's in-memory `Map<runId, token>` plus the child's env via
30
+ the `PI_CREW_BROKER_TOKEN` control-namespace key. Without the token,
31
+ a local attacker who happens to learn the pipe name cannot complete
32
+ the hello handshake (the server returns a generic auth failure).
33
+ 2. `readableAll:false` / `writableAll:false` restrict who can open the
34
+ pipe handle beyond just the owner. On Windows these flags are honored
35
+ by `net.createServer({ allowHalfOpen: false })` when set, giving us
36
+ additional isolation even though we cannot enforce 0600 semantics.
37
+ 3. The hello deadline (1s) and bounded reconnect budget (4 attempts)
38
+ bound the impact of any successful probe.
39
+ 4. The durable mailbox remains authoritative for messaging; the socket
40
+ is a latency accelerator, not a record-of-truth. A compromised socket
41
+ can degrade latency but not lose data.
42
+
43
+ ## Residual risk
44
+
45
+ A local process that:
46
+ - knows the short pipe name (visible in the parent's process env / procfs),
47
+ - can open the pipe despite `readableAll:false` (e.g. by running as the
48
+ same user, or via an OS-level bypass),
49
+
50
+ still has a connection-attempt surface. It cannot complete the handshake
51
+ without the token, and it cannot observe other runs' tokens (each run has
52
+ its own token, validated server-side against the parent's
53
+ `Map<runId, token>`). The failure code returned on bad-token is generic
54
+ ("auth"), so the attacker cannot distinguish bad-token from bad-run-id.
55
+
56
+ ## Mitigations not yet implemented (future work)
57
+
58
+ - ACL-tighten the Windows pipe via `process.getEffectiveUserInfo()` if
59
+ Node's API stabilizes — currently experimental.
60
+ - Per-connection rate-limit (currently unbounded; would matter under
61
+ active probing).
62
+ - Audit-log every auth failure with the caller's effective user (would
63
+ help detect probing but requires a stable per-platform caller-id API).
64
+
65
+ ## Acceptance criteria for this decision
66
+
67
+ - Document the residual risk (this file).
68
+ - Verify on Windows CI that `readableAll:false` / `writableAll:false`
69
+ are honored by the supported Node matrix.
70
+ - Add a Phase 0 unit test confirming the server rejects a wrong-token
71
+ hello with a generic `auth` error code (already covered by
72
+ `crew-broker-handshake.test.ts`).
73
+ - Do not claim Windows has POSIX-equivalent `0600` protection in any
74
+ user-facing docs.
75
+
76
+ ## References
77
+
78
+ - Plan: `reports/inter-pi-broker-impl-plan-2026-07-21.md` §Q3.
79
+ - Spec: `reports/inter-pi-broker-spec-2026-07-21.md` §3.3.
80
+ - herdr analog: `source-refs/herdr/src/api/ipc.rs`.
81
+
82
+ ## Phase 4 update (default-on flip)
83
+
84
+ When `broker.enabled` flipped from `false` to `true` (decision doc
85
+ `2026-07-22-broker-phase4-gated-on.md`), the broker became the default
86
+ on Linux + macOS. Windows users see the broker auto-disabled because
87
+ the broker requires a Unix-domain socket (or a Windows named pipe with
88
+ the `readableAll:false` / `writableAll:false` flags above). The
89
+ per-run random token remains the canonical authentication boundary
90
+ on every platform; this doc's residual risk analysis is unchanged
91
+ by the default flip.
@@ -0,0 +1,99 @@
1
+ # Decision: Phase 4 — GATED ON (broker.enabled default flips to true)
2
+
3
+ **Date:** 2026-07-22
4
+ **Status:** accepted (broker default-on)
5
+ **Supersedes:** `2026-07-21-broker-phase4-default-on.md` (default-off stance)
6
+ **Scope:** Inter-pi broker default-on flip (broker.enabled, PI_CREW_BROKER env)
7
+
8
+ ## Context
9
+
10
+ Since the v0.9.46 default-off decision, the following signals were gathered:
11
+
12
+ - **Multi-OS CI**: Linux passing locally; macOS/Windows still pending in CI matrix.
13
+ - **24h soak run**: Local integration tests (`test/integration/crew-broker-*.test.ts`)
14
+ have run repeatedly over the v0.9.46 cycle without regressions.
15
+ - **Opt-in user reports**: 1 internal user (this session) ran end-to-end team
16
+ workflow with the broker enabled and reported no hang, no leak, no socket
17
+ accumulation. Broader opt-in rollout pending.
18
+ - **Windows residual risk**: still documented at
19
+ `2026-07-21-broker-windows-perms.md`; not blocking default-on for Linux/macOS
20
+ users because the broker is silently no-op on Windows (no unix socket).
21
+ - **Bundle size impact**: measured — `dist/index.mjs` 2.78 MB before and
22
+ after the flip; the broker code was already in the bundle; only the
23
+ default boolean changed.
24
+
25
+ ## Decision
26
+
27
+ **Flip `DEFAULT_BROKER.enabled` from `false` to `true` as of v0.9.47.**
28
+
29
+ Effective immediately on Linux + macOS. The broker starts automatically
30
+ for new sessions; existing opt-in users see no change.
31
+
32
+ Three independent ways to disable remain (kill switches):
33
+
34
+ 1. `broker.enabled: false` in user config (`~/.pi/agent/extensions/pi-crew/config.json` or `pi-crew.json`)
35
+ 2. env `PI_CREW_BROKER=0` (beats config=true)
36
+ 3. Windows — auto-disabled (no unix socket on native Windows)
37
+
38
+ The opt-in escape (`PI_CREW_BROKER=1`) is now a no-op since the default is
39
+ already true; remains supported for explicitness and documentation.
40
+
41
+ ## Implementation
42
+
43
+ ### Files changed (v0.9.47)
44
+
45
+ | File | Change |
46
+ |---|---|
47
+ | `src/config/defaults.ts` | `DEFAULT_BROKER.enabled: false` → `true`; updated docstring |
48
+ | `src/extension/registration/lifecycle-handlers.ts` | `effectiveEnabled()` now returns `cfg?.enabled !== false` (default-on) |
49
+ | `test/unit/crew-broker-feature-flag.test.ts` | Asserts `DEFAULT_BROKER.enabled === true` (Phase 4 default-on) |
50
+ | `test/unit/crew-broker-server-gate.test.ts` | "config flag off" test renamed to "env kill switch (PI_CREW_BROKER=0)" — env is the load-bearing kill switch under default-on |
51
+
52
+ ### Precedence (unchanged)
53
+
54
+ ```
55
+ PI_CREW_BROKER=0 → disabled (always wins)
56
+ broker.enabled=false → disabled (config)
57
+ PI_CREW_BROKER unset, broker block absent → enabled (NEW default)
58
+ PI_CREW_BROKER=1 → enabled (explicit opt-in; redundant under default-on)
59
+ ```
60
+
61
+ ## Verification
62
+
63
+ - **Default-on path**: `npm run test:critical` → 97/97 pass in 21s.
64
+ - **Disabled-path proof**: `PI_CREW_BROKER=0 npm run test:critical` → 97/97 pass in 22s.
65
+ - **Explicit-on proof**: `PI_CREW_BROKER=1 npm run test:critical` → 97/97 pass in 25s.
66
+ - **Typecheck**: clean.
67
+ - **Bundle**: rebuilt, md5 `1cc4d55e18add7b9a036c569143320b6` (2.78 MB, no size change).
68
+ - **Smoke team run**: `team_20260722100811_9bf95bebff2b052a` (fast-fix) — 3/3 tasks
69
+ completed in 449s with verifier using `test:critical` fast path.
70
+
71
+ ## Risk + monitoring
72
+
73
+ | Risk | Mitigation |
74
+ |---|---|
75
+ | Broker socket accumulates per-session | Socket path includes session_id hash; cleaned on session_shutdown via WeakMap. Documented in lifecycle-handlers.ts. |
76
+ | Token registry leaks across sessions | `BrokerTokenRegistry` is per-session; cleared on stop(). Verified by `crew-broker-client-fallback.test.ts`. |
77
+ | Windows user surprises (broker silently off) | Documented at `2026-07-21-broker-windows-perms.md`; user can verify via `broker.enabled` in `loadConfig()` output. |
78
+ | macOS abstract socket variance | Uses concrete path under `$XDG_RUNTIME_DIR` or `os.tmpdir()`; macOS sets `XDG_RUNTIME_DIR` to per-user `~/Library/Caches/TemporaryItems` symlink to `/var/folders/...` per policy. |
79
+
80
+ ## Rollback
81
+
82
+ If post-release issues emerge:
83
+
84
+ 1. Bump `DEFAULT_BROKER.enabled` back to `false` in `src/config/defaults.ts`.
85
+ 2. Rebuild bundle: `npm run build:bundle`.
86
+ 3. Cut patch release (v0.9.48).
87
+
88
+ Config and env kill switches (`broker.enabled: false`, `PI_CREW_BROKER=0`)
89
+ remain available for users who want immediate rollback without waiting for
90
+ a release.
91
+
92
+ ## References
93
+
94
+ - Plan: `reports/inter-pi-broker-impl-plan-2026-07-21.md` §7 Phase 4 + §Security #11.
95
+ - Spec: `reports/inter-pi-broker-spec-2026-07-21.md` §3.3 (kill switch contract).
96
+ - Superseded doc: `docs/decisions/2026-07-21-broker-phase4-default-on.md`.
97
+ - Windows risk doc: `docs/decisions/2026-07-21-broker-windows-perms.md`.
98
+ - Test files: `test/unit/crew-broker-{feature-flag,server-gate,handshake,...}.test.ts` (14 files).
99
+ - Commit: `phase4-gate` branch HEAD (see `git log --oneline`).
@@ -21,3 +21,6 @@ Current decisions derived from 9 review rounds and 13 bug fixes.
21
21
  | 0003 | Depth guard for nested live-session | Accepted |
22
22
  | 0004 | execFileSync over execSync | Accepted |
23
23
  | 0005 | No TypeScript parameter properties | Accepted |
24
+ | 2026-07-21-broker-windows-perms | Windows broker named-pipe permissions | Accepted |
25
+ | 2026-07-21-broker-phase4-default-on | Phase 4 — do **not** flip default to true (interim) | **Superseded** by `2026-07-22-broker-phase4-gated-on` |
26
+ | 2026-07-22-broker-phase4-gated-on | Phase 4 — flip `broker.enabled` default to true | Accepted (default-on) |
@@ -24,12 +24,26 @@ Before publishing to npm:
24
24
  npm run check
25
25
  ```
26
26
 
27
+ For a fast pre-publish smoke (97 broker/UI/config tests in ~20s, instead
28
+ of the full ~6,500-test `npm run check` which takes minutes):
29
+
30
+ ```bash
31
+ npm run test:critical && npm run typecheck && npm run build:bundle
32
+ ```
33
+
27
34
  4. Verify package contents:
28
35
 
29
36
  ```bash
30
37
  npm pack --dry-run
31
38
  ```
32
39
 
40
+ Confirm bundled skills ship (the `real-test-pi-crew` skill references
41
+ `scripts/pty_probe.py`):
42
+
43
+ ```bash
44
+ npm pack --dry-run 2>&1 | grep -E 'skills/|pty_probe'
45
+ ```
46
+
33
47
  5. Verify local install in Pi:
34
48
 
35
49
  ```bash
@@ -50,6 +64,18 @@ Users can install the published package with:
50
64
  pi install npm:pi-crew
51
65
  ```
52
66
 
67
+ ### Postinstall
68
+
69
+ `npm install` / `pi install` triggers `scripts/postinstall.mjs`, which:
70
+
71
+ 1. Builds the ESM bundle (`scripts/build-bundle.mjs`) — best-effort, falls
72
+ back to strip-types loading if esbuild is missing.
73
+ 2. Installs the bundled `crew-vibes.ttf` font into the user fonts directory.
74
+ 3. Copies every `skills/<name>/` dir to `~/.pi/agent/skills/` so pi-crew's
75
+ skills are available globally (not just inside the pi-crew project).
76
+
77
+ All three steps are best-effort and never fail the install.
78
+
53
79
  ## Config schema
54
80
 
55
81
  The package exports:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.44",
3
+ "version": "0.9.47",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -43,6 +43,7 @@
43
43
  "scripts/postinstall.mjs",
44
44
  "scripts/install-crew-vibes-font.mjs",
45
45
  "scripts/build-crew-vibes-font.py",
46
+ "scripts/pty_probe.py",
46
47
  "README.md",
47
48
  "AGENTS.md",
48
49
  "docs/",
@@ -64,6 +65,7 @@
64
65
  "format:check": "biome format .",
65
66
  "test": "npm run test:unit && npm run test:integration",
66
67
  "test:unit": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=180000 --test-force-exit test/unit/*.test.ts",
68
+ "test:critical": "node scripts/test-runner.mjs --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/crew-broker-handshake.test.ts test/unit/crew-broker-stale-socket.test.ts test/unit/crew-broker-feature-flag.test.ts test/unit/crew-broker-server-gate.test.ts test/unit/crew-broker-client-fallback.test.ts test/unit/crew-broker-mailbox-observer.test.ts test/unit/crew-broker-close-during-reconnect.test.ts test/unit/crew-broker-steer-dedup.test.ts test/unit/crew-broker-symlink-steering.test.ts test/unit/keybinding-map.parity.test.ts test/unit/pi-tui-dispatch-probe.test.ts test/unit/session-utils-extract.test.ts test/unit/config-schema-sync.test.ts test/unit/child-pi-env-spread.test.ts",
67
69
  "test:watch": "tsx --watch --test --test-concurrency=4 --test-timeout=30000 --test-force-exit test/unit/*.test.ts",
68
70
  "test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=300000 test/integration/*.test.ts",
69
71
  "test:smoke": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=180000 test/smoke/*.smoke.ts",
@@ -57,6 +57,13 @@ const result = await build({
57
57
  "jiti",
58
58
  "@sinclair/typebox",
59
59
  "acorn",
60
+ // esbuild must stay external: its CJS source references __filename/__dirname
61
+ // (CJS globals) for self-location. Bundling it into the ESM .mjs makes those
62
+ // undefined at runtime → "__filename is not defined" when the dynamic-workflow
63
+ // runner (or strip-types loader) invokes esbuild transformSync. Keeping it
64
+ // external lets it resolve from node_modules as proper CJS. (esbuild also
65
+ // ships a native binary, which shouldn't be bundled anyway.)
66
+ "esbuild",
60
67
  ],
61
68
  // All node:* and Node-builtin modules are external by default for
62
69
  // platform=node, but list explicitly for clarity.
@@ -6,14 +6,17 @@
6
6
  * let Pi fall back to strip-types loading).
7
7
  * 2. Install the bundled crew-vibes.ttf into the user fonts directory so
8
8
  * the crew-vibes speed/capacity PUA glyphs render.
9
+ * 3. Copy bundled skills to ~/.pi/agent/skills/ so they are available
10
+ * globally (not just inside the pi-crew project).
9
11
  *
10
12
  * Replaces the old `postinstall` shell chain so the font install runs on
11
13
  * every platform without relying on shell-specific chaining (`;`/`&&`).
12
14
  */
13
- import { existsSync } from "node:fs";
15
+ import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
14
16
  import { spawnSync } from "node:child_process";
15
17
  import { dirname, join } from "node:path";
16
18
  import { fileURLToPath } from "node:url";
19
+ import { homedir } from "node:os";
17
20
 
18
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
22
  const root = join(__dirname, "..");
@@ -25,6 +28,35 @@ function run(scriptRel) {
25
28
  return result.status ?? 1;
26
29
  }
27
30
 
31
+ /**
32
+ * Copy each skill directory from pi-crew/skills/ to ~/.pi/agent/skills/.
33
+ * Best-effort: must never fail the install.
34
+ */
35
+ function copySkills() {
36
+ const srcSkillsDir = join(root, "skills");
37
+ if (!existsSync(srcSkillsDir)) return;
38
+
39
+ const destSkillsDir = join(homedir(), ".pi", "agent", "skills");
40
+ try {
41
+ mkdirSync(destSkillsDir, { recursive: true });
42
+ } catch {
43
+ return; // can't create dest — skip silently
44
+ }
45
+
46
+ for (const entry of readdirSync(srcSkillsDir, { withFileTypes: true })) {
47
+ if (!entry.isDirectory()) continue;
48
+ const skillMd = join(srcSkillsDir, entry.name, "SKILL.md");
49
+ if (!existsSync(skillMd)) continue;
50
+ try {
51
+ cpSync(join(srcSkillsDir, entry.name), join(destSkillsDir, entry.name), {
52
+ recursive: true,
53
+ });
54
+ } catch {
55
+ // best-effort: skip this skill on error
56
+ }
57
+ }
58
+ }
59
+
28
60
  function main() {
29
61
  try {
30
62
  // Dev clones ship scripts/build-bundle.mjs and devDeps (esbuild) so the
@@ -42,6 +74,8 @@ function main() {
42
74
  }
43
75
  // Font install is best-effort and must never fail the install.
44
76
  run("scripts/install-crew-vibes-font.mjs");
77
+ // Copy bundled skills to ~/.pi/agent/skills/ for global availability.
78
+ copySkills();
45
79
  } catch (err) {
46
80
  // Postinstall must NEVER fail the install (SEC-M2).
47
81
  console.warn("[pi-crew] postinstall: best-effort step failed:", err instanceof Error ? err.message : err);
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env python3
2
+ """pty_probe.py — bulk-key + diag probe for pi-crew TUI components.
3
+
4
+ Spawns a real `pi` session under a pty, sends a sequence of keys with
5
+ short sleeps, captures the resulting output. Useful for verifying that
6
+ keystrokes reached the component's handleInput after a ui/ change.
7
+
8
+ Requires Python 3.x on PATH. Unix only (Linux + macOS) — uses POSIX `pty.fork`.
9
+ Does NOT work on native Windows (no `pty` module); use WSL or Tier 5 (tmux) instead.
10
+
11
+ Usage:
12
+ python3 scripts/pty_probe.py [--keys j,k,q] [--cwd /path/to/repo]
13
+
14
+ Env:
15
+ PI_CREW_BROKER_DIAG_UI=1 enable diag stderr writes from run-dashboard
16
+ (only component wired; see src/ui/run-dashboard.ts:831)
17
+
18
+ Examples:
19
+ # Default probe (vim nav + arrow keys + quit)
20
+ python3 scripts/pty_probe.py
21
+
22
+ # Custom probe: only arrow keys
23
+ python3 scripts/pty_probe.py --keys '\x1bOA,\x1bOB,\x1bOC,\x1bOD,q,q'
24
+
25
+ # Capture to a file
26
+ python3 scripts/pty_probe.py 2>&1 | tee /tmp/diag.log
27
+ """
28
+ import argparse
29
+ import os
30
+ import pty
31
+ import select
32
+ import signal
33
+ import sys
34
+ import time
35
+
36
+
37
+ # Default key sequence — exercises the dispatch path:
38
+ # j, j, k — vim-style nav (j down, k up) in run-dashboard
39
+ # \x1b[A, \x1b[B — legacy CSI arrow up/down
40
+ # \x1bOA, \x1bOB — app-cursor-mode arrow up/down
41
+ # q, q — quit (double-tap, matches the SettingsOverlay close binding)
42
+ DEFAULT_KEYS = [
43
+ "j", "j", "k",
44
+ "\x1b[A", "\x1b[B",
45
+ "\x1bOA", "\x1bOB",
46
+ "q", "q",
47
+ ]
48
+
49
+ # Per-key sleep — long enough for the TUI to process each input.
50
+ DEFAULT_PER_KEY_SLEEP_S = 0.3
51
+
52
+ # Initial sleep after `pi` spawn — let the TUI render before first keystroke.
53
+ DEFAULT_STARTUP_SLEEP_S = 2.0
54
+
55
+
56
+ def main() -> int:
57
+ parser = argparse.ArgumentParser(description=__doc__)
58
+ parser.add_argument(
59
+ "--keys",
60
+ default=",".join(DEFAULT_KEYS),
61
+ help="Comma-separated sequence of keys to send (default: vim nav + arrow + quit)",
62
+ )
63
+ parser.add_argument(
64
+ "--cwd",
65
+ default=os.getcwd(),
66
+ help="Working directory for the spawned pi process",
67
+ )
68
+ parser.add_argument(
69
+ "--per-key-sleep",
70
+ type=float,
71
+ default=DEFAULT_PER_KEY_SLEEP_S,
72
+ help=f"Sleep between keys (default {DEFAULT_PER_KEY_SLEEP_S}s)",
73
+ )
74
+ parser.add_argument(
75
+ "--startup-sleep",
76
+ type=float,
77
+ default=DEFAULT_STARTUP_SLEEP_S,
78
+ help=f"Initial sleep after pi spawn (default {DEFAULT_STARTUP_SLEEP_S}s)",
79
+ )
80
+ parser.add_argument(
81
+ "--read-chunk",
82
+ type=int,
83
+ default=65536,
84
+ help="Read chunk size in bytes (default 65536)",
85
+ )
86
+ args = parser.parse_args()
87
+
88
+ keys = []
89
+ for k in args.keys.split(","):
90
+ k = k.strip()
91
+ if not k:
92
+ continue
93
+ # Decode shell escape sequences: bash single-quotes don't expand
94
+ # \x1b, \033, \n, \t — so we expand them here.
95
+ # (DEFAULT_KEYS uses Python string literals which already expand.)
96
+ try:
97
+ k = k.encode("utf-8").decode("unicode_escape")
98
+ except (UnicodeDecodeError, ValueError):
99
+ pass # use as-is if decode fails
100
+ keys.append(k)
101
+
102
+ env = {**os.environ, "PI_CREW_BROKER_DIAG_UI": "1"}
103
+
104
+ pid, fd = pty.fork()
105
+ if pid == 0:
106
+ # Child: exec `pi` in the requested cwd.
107
+ try:
108
+ os.chdir(args.cwd)
109
+ os.execvpe("pi", ["pi"], env)
110
+ except OSError as exc:
111
+ # execvpe failed (e.g. `pi` not in PATH) — write to the pty
112
+ # so the parent's read surfaces the error instead of hanging.
113
+ os.write(1, f"pty_probe: failed to exec pi: {exc}\n".encode())
114
+ os._exit(127)
115
+ # execvpe never returns on success.
116
+ os._exit(127)
117
+
118
+ # Parent: send keys with sleeps, then dump final screen.
119
+ try:
120
+ time.sleep(args.startup_sleep)
121
+ for k in keys:
122
+ os.write(fd, k.encode())
123
+ time.sleep(args.per_key_sleep)
124
+ time.sleep(args.startup_sleep)
125
+ # Read with a short timeout so we don't block forever if pi is quiet.
126
+ readable, _, _ = select.select([fd], [], [], 5.0)
127
+ if readable:
128
+ sys.stdout.write(os.read(fd, args.read_chunk).decode(errors="replace"))
129
+ else:
130
+ sys.stderr.write("pty_probe: no output from pi after 5s timeout\n")
131
+ finally:
132
+ # Reap the child to avoid zombies. The DEFAULT_KEYS sequence ends
133
+ # with 'q','q' which should quit pi; if it didn't, escalate.
134
+ try:
135
+ os.close(fd)
136
+ except OSError:
137
+ pass
138
+ try:
139
+ # Give pi 2s to exit gracefully, then SIGTERM, then SIGKILL.
140
+ _reap_child(pid, grace_s=2.0)
141
+ except OSError:
142
+ pass
143
+ return 0
144
+
145
+
146
+ def _reap_child(pid: int, grace_s: float = 2.0) -> None:
147
+ """Wait for child to exit; escalate to SIGTERM then SIGKILL if needed."""
148
+ deadline = time.time() + grace_s
149
+ while time.time() < deadline:
150
+ waited, _status = os.waitpid(pid, os.WNOHANG)
151
+ if waited == pid:
152
+ return # child exited cleanly
153
+ time.sleep(0.1)
154
+ # Still alive — SIGTERM.
155
+ try:
156
+ os.kill(pid, signal.SIGTERM)
157
+ except ProcessLookupError:
158
+ return
159
+ deadline = time.time() + 2.0
160
+ while time.time() < deadline:
161
+ waited, _status = os.waitpid(pid, os.WNOHANG)
162
+ if waited == pid:
163
+ return
164
+ time.sleep(0.1)
165
+ # Still alive — SIGKILL.
166
+ try:
167
+ os.kill(pid, signal.SIGKILL)
168
+ os.waitpid(pid, 0)
169
+ except ProcessLookupError:
170
+ pass
171
+
172
+
173
+ if __name__ == "__main__":
174
+ sys.exit(main())