pi-crew 0.9.41 → 0.9.44

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 (170) hide show
  1. package/CHANGELOG.md +253 -0
  2. package/README.md +14 -0
  3. package/dist/build-meta.json +6450 -12419
  4. package/dist/index.mjs +57998 -68417
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/migration/atomic-write-v2-migration.md +2 -0
  7. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
  8. package/package.json +4 -2
  9. package/scripts/README.md +6 -0
  10. package/scripts/build-bundle.mjs +2 -1
  11. package/scripts/check-all-skills.ts +3 -3
  12. package/scripts/dev-runner.mjs +106 -0
  13. package/scripts/postinstall.mjs +6 -2
  14. package/scripts/verify-skill.ts +2 -2
  15. package/src/agents/agent-config.ts +1 -1
  16. package/src/config/config.ts +20 -1
  17. package/src/config/drift-detector.ts +97 -15
  18. package/src/config/resilient-parser.ts +4 -4
  19. package/src/config/types.ts +0 -2
  20. package/src/extension/crew-cleanup.ts +17 -3
  21. package/src/extension/crew-vibes/config.ts +1 -1
  22. package/src/extension/crew-vibes/font-detect.ts +16 -3
  23. package/src/extension/crew-vibes/index.ts +1 -1
  24. package/src/extension/cross-extension-rpc.ts +26 -9
  25. package/src/extension/management.ts +6 -5
  26. package/src/extension/register.ts +58 -1573
  27. package/src/extension/registration/command-registration.ts +57 -0
  28. package/src/extension/registration/commands.ts +7 -2
  29. package/src/extension/registration/context-builder.ts +131 -0
  30. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  31. package/src/extension/registration/foreground-run-controller.ts +238 -0
  32. package/src/extension/registration/hook-registration.ts +119 -0
  33. package/src/extension/registration/lazy-configurers.ts +124 -0
  34. package/src/extension/registration/lifecycle-handlers.ts +731 -0
  35. package/src/extension/registration/observability.ts +33 -4
  36. package/src/extension/registration/registration-types.ts +149 -0
  37. package/src/extension/registration/runtime-cleanup.ts +230 -0
  38. package/src/extension/registration/subagent-manager-setup.ts +211 -0
  39. package/src/extension/registration/tool-registration.ts +50 -0
  40. package/src/extension/registration/wire-cross-extension.ts +37 -0
  41. package/src/extension/rpc-hmac.ts +21 -1
  42. package/src/extension/run-import.ts +3 -3
  43. package/src/extension/team-tool/auto-summarize.ts +4 -4
  44. package/src/extension/team-tool/cancel.ts +5 -5
  45. package/src/extension/team-tool/chain-executor.ts +5 -4
  46. package/src/extension/team-tool/context.ts +11 -1
  47. package/src/extension/team-tool/doctor.ts +2 -1
  48. package/src/extension/team-tool/goal-wrap.ts +2 -2
  49. package/src/extension/team-tool/goal.ts +1 -1
  50. package/src/extension/team-tool/handle-schedule.ts +25 -36
  51. package/src/extension/team-tool/orchestrate.ts +0 -1
  52. package/src/extension/team-tool/parallel-dispatch.ts +5 -0
  53. package/src/extension/team-tool/run.ts +220 -214
  54. package/src/extension/team-tool/workflow-manage.ts +1 -1
  55. package/src/extension/team-tool.ts +69 -16
  56. package/src/hooks/registry.ts +7 -2
  57. package/src/observability/correlation.ts +4 -3
  58. package/src/observability/event-bus.ts +10 -1
  59. package/src/observability/event-to-metric.ts +8 -7
  60. package/src/observability/exporters/adapter.ts +6 -9
  61. package/src/observability/exporters/otlp-exporter.ts +81 -0
  62. package/src/observability/metric-retention.ts +4 -0
  63. package/src/observability/metric-sink.ts +14 -4
  64. package/src/prompt/prompt-runtime.ts +15 -0
  65. package/src/runtime/adaptive-plan.ts +1 -1
  66. package/src/runtime/async-runner.ts +13 -4
  67. package/src/runtime/background-runner.ts +32 -23
  68. package/src/runtime/capability-inventory.ts +1 -1
  69. package/src/runtime/chain-runner.ts +5 -5
  70. package/src/runtime/checkpoint.ts +6 -16
  71. package/src/runtime/child-pi-constants.ts +42 -0
  72. package/src/runtime/child-pi-kill.ts +180 -0
  73. package/src/runtime/child-pi-spawn.ts +234 -0
  74. package/src/runtime/child-pi-steering.ts +128 -0
  75. package/src/runtime/child-pi-streams.ts +296 -0
  76. package/src/runtime/child-pi-transcript.ts +169 -0
  77. package/src/runtime/child-pi.ts +80 -822
  78. package/src/runtime/coalesce-tasks.ts +1 -1
  79. package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
  80. package/src/runtime/crash-recovery.ts +63 -47
  81. package/src/runtime/delivery-coordinator.ts +1 -1
  82. package/src/runtime/deterministic-ast.ts +2 -2
  83. package/src/runtime/diagnostic-export.ts +2 -1
  84. package/src/runtime/dwf-state-store.ts +5 -0
  85. package/src/runtime/dynamic-workflow-context.ts +78 -18
  86. package/src/runtime/dynamic-workflow-runner.ts +18 -2
  87. package/src/runtime/foreground-control.ts +3 -2
  88. package/src/runtime/goal-evaluator.ts +59 -27
  89. package/src/runtime/goal-loop-runner.ts +46 -13
  90. package/src/runtime/goal-state-store.ts +1 -1
  91. package/src/runtime/handoff-manager.ts +0 -2
  92. package/src/runtime/intercom-bridge.ts +0 -2
  93. package/src/runtime/live-agent-manager.ts +30 -5
  94. package/src/runtime/live-extension-bridge.ts +0 -2
  95. package/src/runtime/live-session-runtime.ts +65 -57
  96. package/src/runtime/manifest-cache.ts +59 -0
  97. package/src/runtime/mcp-proxy.ts +1 -1
  98. package/src/runtime/orphan-worker-registry.ts +3 -3
  99. package/src/runtime/output-validator.ts +1 -1
  100. package/src/runtime/path-overlap.ts +1 -1
  101. package/src/runtime/pi-args.ts +3 -3
  102. package/src/runtime/pi-json-output.ts +4 -3
  103. package/src/runtime/pi-spawn.ts +7 -1
  104. package/src/runtime/pipeline-runner.ts +1 -4
  105. package/src/runtime/post-checks.ts +0 -1
  106. package/src/runtime/retry-runner.ts +1 -1
  107. package/src/runtime/run-coalesced-task-group.ts +18 -23
  108. package/src/runtime/scheduler.ts +2 -0
  109. package/src/runtime/session-resources.ts +1 -1
  110. package/src/runtime/settings-store.ts +31 -1
  111. package/src/runtime/skill-effectiveness.ts +2 -2
  112. package/src/runtime/stale-reconciler.ts +23 -53
  113. package/src/runtime/subagent-manager.ts +19 -3
  114. package/src/runtime/task-output-context.ts +2 -1
  115. package/src/runtime/task-runner/prompt-builder.ts +3 -3
  116. package/src/runtime/task-runner/run-projection.ts +3 -1
  117. package/src/runtime/task-runner/state-helpers.ts +13 -1
  118. package/src/runtime/task-runner.ts +31 -6
  119. package/src/runtime/team-runner.ts +8 -2
  120. package/src/runtime/verification-gates.ts +54 -5
  121. package/src/schema/config-schema.ts +19 -1
  122. package/src/schema/team-tool-schema.ts +2 -0
  123. package/src/skills/discover-skills.ts +25 -15
  124. package/src/state/active-run-registry.ts +0 -1
  125. package/src/state/atomic-write.ts +37 -11
  126. package/src/state/blob-store.ts +3 -9
  127. package/src/state/contracts.ts +5 -1
  128. package/src/state/crew-init.ts +3 -2
  129. package/src/state/decision-ledger.ts +2 -2
  130. package/src/state/event-log-rotation.ts +1 -1
  131. package/src/state/event-log.ts +4 -3
  132. package/src/state/gitignore-manager.ts +2 -1
  133. package/src/state/health-store.ts +5 -2
  134. package/src/state/instinct-store.ts +11 -2
  135. package/src/state/locks.ts +132 -49
  136. package/src/state/mailbox.ts +3 -4
  137. package/src/state/observation-store.ts +1 -13
  138. package/src/state/run-cache.ts +12 -12
  139. package/src/state/run-graph.ts +2 -1
  140. package/src/state/run-metrics.ts +1 -1
  141. package/src/state/schedule.ts +7 -1
  142. package/src/state/state-store.ts +9 -3
  143. package/src/state/tiered-eval.ts +1 -1
  144. package/src/state/worker-atomic-writer.ts +1 -0
  145. package/src/ui/dynamic-border.ts +13 -3
  146. package/src/ui/live-conversation-overlay.ts +7 -27
  147. package/src/ui/live-duration.ts +2 -2
  148. package/src/ui/live-run-sidebar.ts +56 -32
  149. package/src/ui/mascot.ts +19 -13
  150. package/src/ui/powerbar-publisher.ts +244 -178
  151. package/src/ui/render-coalescer.ts +44 -3
  152. package/src/ui/render-diff.ts +2 -2
  153. package/src/ui/run-dashboard.ts +122 -21
  154. package/src/ui/run-event-bus.ts +66 -0
  155. package/src/ui/run-snapshot-cache.ts +84 -81
  156. package/src/ui/settings-overlay.ts +3 -3
  157. package/src/ui/status-colors.ts +24 -0
  158. package/src/ui/tool-progress-formatter.ts +1 -1
  159. package/src/ui/tool-render.ts +3 -3
  160. package/src/ui/transcript-cache.ts +5 -10
  161. package/src/ui/transcript-entries.ts +68 -72
  162. package/src/ui/transcript-viewer.ts +54 -14
  163. package/src/ui/widget/index.ts +26 -15
  164. package/src/ui/widget/widget-model.ts +18 -9
  165. package/src/ui/widget/widget-renderer.ts +2 -5
  166. package/src/utils/fingerprint.ts +1 -1
  167. package/src/utils/paths.ts +1 -1
  168. package/src/utils/project-detector.ts +1 -1
  169. package/src/worktree/worktree-manager.ts +47 -19
  170. package/src/state/atomic-write-v2.ts +0 -108
@@ -1,5 +1,7 @@
1
1
  # atomic-write-v2 migration plan
2
2
 
3
+ > **Note:** `atomic-write-v2.ts` was consolidated into `atomic-write.ts` as of v0.9.42. This document is preserved as historical record — the migration was completed with v1 winning on simplicity + symlink-safety + link+unlink atomicity. The v2 class was never adopted; no v2 API surface ships with pi-crew.
4
+
3
5
  > Status: **PROPOSED** — decision RECOMMEND MIGRATE (staged, feature-flagged).
4
6
  > Source: performance review `docs/perf/performance-review-2026-07.md` (findings F3–F6).
5
7
  > Scope: replace the standalone-function module `src/state/atomic-write.ts`
@@ -1,5 +1,7 @@
1
1
  # Fallow Patterns Adoption - Implementation Plan
2
2
 
3
+ > **Note:** `atomic-write-v2` / `AtomicWriter` mentioned in this plan was consolidated into `atomic-write.ts` as of v0.9.42. This document is preserved as historical record — the v2 class was never adopted; v1 won on simplicity + symlink-safety + link+unlink atomicity. See `docs/migration/atomic-write-v2-migration.md` for the decision rationale.
4
+
3
5
  > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
6
 
5
7
  **Goal:** Adopt 5 high-value patterns from fallow (Rust static analyzer) into pi-crew: structured error codes, atomic write v2 with fsync, health score system, plugin registry, and stable task ID refinement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.41",
3
+ "version": "0.9.44",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  ],
56
56
  "scripts": {
57
57
  "check": "npm run ci",
58
- "ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:lazy-imports && npm run check:bundle-staleness && npm test && npm pack --dry-run",
58
+ "ci": "npm run typecheck && npm run lint && npm run format:check && npm run check:conflict-markers && npm run check:lazy-imports && npm run check:bundle-staleness && npm run build:bundle && npm run test:bundle && npm test && npm pack --dry-run",
59
59
  "check:lazy-imports": "node scripts/check-lazy-imports.mjs",
60
60
  "check:bundle-staleness": "node scripts/check-bundle-staleness.mjs",
61
61
  "check:conflict-markers": "node scripts/check-conflict-markers.mjs",
@@ -68,8 +68,10 @@
68
68
  "test:integration": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=300000 test/integration/*.test.ts",
69
69
  "test:smoke": "node scripts/test-runner.mjs --test-concurrency=1 --test-timeout=180000 test/smoke/*.smoke.ts",
70
70
  "build:bundle": "node scripts/build-bundle.mjs",
71
+ "test:bundle": "node --experimental-strip-types --test --test-force-exit test/unit/bundle-load.test.ts",
71
72
  "watch:bundle": "node scripts/watch-bundle.mjs",
72
73
  "build:bundle:once": "node scripts/watch-bundle.mjs --once",
74
+ "dev": "node scripts/dev-runner.mjs",
73
75
  "postinstall": "node scripts/postinstall.mjs",
74
76
  "install:crew-font": "node scripts/install-crew-vibes-font.mjs",
75
77
  "build:crew-font": "python scripts/build-crew-vibes-font.py",
@@ -0,0 +1,6 @@
1
+ # Scripts
2
+
3
+ > **Note:** Many scripts in this directory are one-off fix scripts (e.g.,
4
+ > `fix-*.cjs`, `patch-*.cjs`) that were used for specific migrations or
5
+ > bugfixes and could be archived. Only `postinstall.mjs`, `build-bundle.mjs`,
6
+ > and `watch-bundle.mjs` are actively used by the project.
@@ -55,7 +55,8 @@ const result = await build({
55
55
  "cli-highlight",
56
56
  "diff",
57
57
  "jiti",
58
- "typebox",
58
+ "@sinclair/typebox",
59
+ "acorn",
59
60
  ],
60
61
  // All node:* and Node-builtin modules are external by default for
61
62
  // platform=node, but list explicitly for clarity.
@@ -9,9 +9,9 @@
9
9
  * node scripts/check-all-skills.ts
10
10
  */
11
11
 
12
- import { spawnSync } from "child_process";
13
- import * as fs from "fs";
14
- import * as path from "path";
12
+ import { spawnSync } from "node:child_process";
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
15
 
16
16
  interface SkillSummary {
17
17
  name: string;
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Dev runner — spawn `watch:bundle` and `test:watch` in parallel.
4
+ *
5
+ * Replaces the `npm-run-all --parallel watch:bundle test:watch` form that
6
+ * would require an additional dev-dependency. Uses Node's stdlib
7
+ * `child_process.spawn` so we keep the dev tooling dependency-free.
8
+ *
9
+ * Behavior:
10
+ * - Spawns both child processes with stdio: inherit (the parent's TTY
11
+ * sees both streams interleaved).
12
+ * - Forwards Ctrl+C (SIGINT) and SIGTERM to both children so they shut
13
+ * down cleanly when the user aborts.
14
+ * - Exits with a non-zero code if EITHER child exits with a non-zero
15
+ * status (mimics `npm-run-all --parallel` semantics).
16
+ *
17
+ * Usage:
18
+ * npm run dev
19
+ * node scripts/dev-runner.mjs
20
+ */
21
+
22
+ import { spawn } from "node:child_process";
23
+ import process from "node:process";
24
+
25
+ const tasks = [
26
+ { name: "watch:bundle", script: "watch:bundle" },
27
+ { name: "test:watch", script: "test:watch" },
28
+ ];
29
+
30
+ /**
31
+ * @typedef {{name: string, child: import("node:child_process").ChildProcess, exited: boolean, code: number|null, signal: NodeJS.Signals|null}} Task
32
+ */
33
+
34
+ /** @type {Task[]} */
35
+ const children = tasks.map((t) => ({
36
+ name: t.name,
37
+ child: spawnNpm(t.script),
38
+ exited: false,
39
+ code: null,
40
+ signal: null,
41
+ }));
42
+
43
+ let shuttingDown = false;
44
+ let exitCode = 0;
45
+
46
+ function spawnNpm(script) {
47
+ return spawn("npm", ["run", "--silent", script], {
48
+ stdio: "inherit",
49
+ env: process.env,
50
+ });
51
+ }
52
+
53
+ function maybeExit() {
54
+ if (!shuttingDown) return;
55
+ if (children.some((c) => !c.exited)) return;
56
+ process.exit(exitCode);
57
+ }
58
+
59
+ function shutdown(reason) {
60
+ if (shuttingDown) return;
61
+ shuttingDown = true;
62
+ process.stderr.write(`\n[dev-runner] received ${reason}, forwarding to children...\n`);
63
+ for (const c of children) {
64
+ if (c.exited) continue;
65
+ try {
66
+ c.child.kill("SIGTERM");
67
+ } catch {
68
+ /* already dead */
69
+ }
70
+ }
71
+ // Hard-kill grace window: if children ignore SIGTERM for 3s, send SIGKILL.
72
+ setTimeout(() => {
73
+ for (const c of children) {
74
+ if (c.exited) continue;
75
+ try {
76
+ c.child.kill("SIGKILL");
77
+ } catch {
78
+ /* already dead */
79
+ }
80
+ }
81
+ }, 3000).unref();
82
+ maybeExit();
83
+ }
84
+
85
+ for (const c of children) {
86
+ c.child.on("exit", (code, signal) => {
87
+ c.exited = true;
88
+ c.code = code;
89
+ c.signal = signal;
90
+ const note = signal ? `signal=${signal}` : `code=${code}`;
91
+ process.stderr.write(`[dev-runner] ${c.name} exited (${note})\n`);
92
+ // Non-zero is only meaningful when the child exited via code (not signal).
93
+ if (signal === null && code !== 0) exitCode = exitCode || (code ?? 1);
94
+ maybeExit();
95
+ });
96
+
97
+ c.child.on("error", (err) => {
98
+ process.stderr.write(`[dev-runner] failed to spawn ${c.name}: ${err.message}\n`);
99
+ c.exited = true;
100
+ exitCode = exitCode || 1;
101
+ maybeExit();
102
+ });
103
+ }
104
+
105
+ process.on("SIGINT", () => shutdown("SIGINT"));
106
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
@@ -32,8 +32,12 @@ function main() {
32
32
  // dist/index.mjs, so this best-effort build simply no-ops.
33
33
  const bundleStatus = run("scripts/build-bundle.mjs");
34
34
  if (bundleStatus !== 0) {
35
- console.warn(
36
- "[pi-crew] postinstall: bundle build skipped or failed; using committed dist/ (or strip-types fallback). Run npm run build:bundle to retry.",
35
+ // TB-11: Surface bundle failures loudly to stderr instead of console.warn,
36
+ // which can scroll off in noisy install logs. We still don't `process.exit(1)`
37
+ // here — the install itself must succeed via the strip-types fallback —
38
+ // but the failure must be obvious to whoever runs `npm install`.
39
+ process.stderr.write(
40
+ "\u001b[31m[pi-crew] postinstall: bundle build FAILED\u001b[0m — using committed dist/ (or strip-types fallback). Run 'npm run build:bundle' to retry. See logs above for esbuild errors.\n",
37
41
  );
38
42
  }
39
43
  // Font install is best-effort and must never fail the install.
@@ -12,8 +12,8 @@
12
12
  * node scripts/verify-skill.ts skills/ # batch mode
13
13
  */
14
14
 
15
- import * as fs from "fs";
16
- import * as path from "path";
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
17
 
18
18
  interface Gate {
19
19
  type: "red" | "green";
@@ -83,7 +83,7 @@ export interface AgentConfig {
83
83
  contextMode?: "fresh" | "fork";
84
84
  /** Maximum turns for this agent. Overrides runtime config if set. */
85
85
  maxTurns?: number;
86
- /** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT_TOKENS env in child. */
86
+ /** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT env in child. */
87
87
  maxTokens?: number;
88
88
  /** Effort level for this agent. Controls how much work the agent puts in. */
89
89
  effort?: "low" | "medium" | "high";
@@ -169,7 +169,7 @@ export function invalidateConfigCache(): void {
169
169
  }
170
170
 
171
171
  function resolveHomeDir(): string {
172
- const envValue = process.env.PI_TEAMS_HOME?.trim();
172
+ const envValue = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
173
173
  const defaultHome = os.homedir();
174
174
  if (!envValue) return defaultHome;
175
175
  // FIX (Round 14): When PI_TEAMS_HOME is explicitly set, validate that
@@ -676,6 +676,7 @@ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
676
676
  maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
677
677
  maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
678
678
  heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
679
+ serializeOnPathOverlap: parseWithSchema(Type.Boolean(), obj.serializeOnPathOverlap),
679
680
  };
680
681
  return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
681
682
  }
@@ -717,6 +718,17 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
717
718
  Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
718
719
  obj.effectivenessGuard,
719
720
  ),
721
+ yield: (() => {
722
+ const y = asRecord(obj.yield);
723
+ if (!y) return undefined;
724
+ const parsed: NonNullable<CrewRuntimeConfig["yield"]> = {
725
+ enabled: parseWithSchema(Type.Boolean(), y.enabled),
726
+ maxReminders: parseWithSchema(Type.Integer({ minimum: 0 }), y.maxReminders),
727
+ reminderPrompt: parseWithSchema(Type.String({ maxLength: 1000 }), y.reminderPrompt),
728
+ };
729
+ return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
730
+ })(),
731
+ excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
720
732
  isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
721
733
  };
722
734
  return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
@@ -815,6 +827,7 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
815
827
  dashboardLiveRefreshMs: parseIntegerInRange(obj.dashboardLiveRefreshMs, 250, 60_000),
816
828
  autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
817
829
  autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
830
+ autoCloseDashboardMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoCloseDashboardMs),
818
831
  showModel: parseWithSchema(Type.Boolean(), obj.showModel),
819
832
  showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
820
833
  showTools: parseWithSchema(Type.Boolean(), obj.showTools),
@@ -932,6 +945,11 @@ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefin
932
945
  autoRecover: parseWithSchema(Type.Boolean(), obj.autoRecover),
933
946
  deadletterThreshold: parsePositiveInteger(obj.deadletterThreshold),
934
947
  cleanupOrphanedTempDirs: parseWithSchema(Type.Boolean(), obj.cleanupOrphanedTempDirs),
948
+ autoRepairIntervalMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoRepairIntervalMs),
949
+ forcePreflight: parseWithSchema(Type.Boolean(), obj.forcePreflight),
950
+ ambientStatusInjection: parseWithSchema(Type.Boolean(), obj.ambientStatusInjection),
951
+ perWriteValidation: parseWithSchema(Type.Boolean(), obj.perWriteValidation),
952
+ scopeModels: parseWithSchema(Type.Boolean(), obj.scopeModels),
935
953
  };
936
954
  return Object.values(reliability).some((entry) => entry !== undefined) ? reliability : undefined;
937
955
  }
@@ -985,6 +1003,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
985
1003
  executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
986
1004
  notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
987
1005
  requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
1006
+ ignoreMethod: parseWithSchema(Type.Union([Type.Literal("gitignore"), Type.Literal("exclude")]), obj.ignoreMethod),
988
1007
  autonomous: parseAutonomousConfig(obj.autonomous),
989
1008
  limits: parseLimitsConfig(obj.limits),
990
1009
  runtime: parseRuntimeConfig(obj.runtime),
@@ -50,10 +50,92 @@ export interface ConfigReferences {
50
50
  workflows: Set<string>;
51
51
  }
52
52
 
53
+ const MAX_CONFIG_KEY_DEPTH = 3;
54
+
55
+ type ConfigSchemaNode = Record<string, unknown>;
56
+
57
+ interface SchemaKeyResolution {
58
+ allowed: boolean;
59
+ childSchema?: unknown;
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function schemaAlternatives(schema: unknown): ConfigSchemaNode[] {
67
+ if (!isRecord(schema)) return [];
68
+ const alternatives = Array.isArray(schema.anyOf) ? schema.anyOf.flatMap((entry) => schemaAlternatives(entry)) : [];
69
+ return [schema, ...alternatives];
70
+ }
71
+
72
+ function resolveSchemaKey(schema: unknown, key: string): SchemaKeyResolution {
73
+ let sawObjectSchema = false;
74
+ let allowsAdditionalProperties = false;
75
+ const childSchemas: unknown[] = [];
76
+
77
+ for (const candidate of schemaAlternatives(schema)) {
78
+ const properties = isRecord(candidate.properties) ? candidate.properties : undefined;
79
+ const patternProperties = isRecord(candidate.patternProperties) ? candidate.patternProperties : undefined;
80
+ if (candidate.type !== "object" && !properties && !patternProperties) continue;
81
+ sawObjectSchema = true;
82
+
83
+ if (properties && Object.hasOwn(properties, key)) {
84
+ childSchemas.push(properties[key]);
85
+ continue;
86
+ }
87
+
88
+ let matchedPattern = false;
89
+ if (patternProperties) {
90
+ for (const [pattern, childSchema] of Object.entries(patternProperties)) {
91
+ try {
92
+ if (new RegExp(pattern).test(key)) {
93
+ childSchemas.push(childSchema);
94
+ matchedPattern = true;
95
+ break;
96
+ }
97
+ } catch {
98
+ // Schema patterns are trusted constants; an invalid one simply cannot match.
99
+ }
100
+ }
101
+ }
102
+ if (!matchedPattern && candidate.additionalProperties !== false) {
103
+ allowsAdditionalProperties = true;
104
+ }
105
+ }
106
+
107
+ return {
108
+ allowed: !sawObjectSchema || childSchemas.length > 0 || allowsAdditionalProperties,
109
+ childSchema: childSchemas[0],
110
+ };
111
+ }
112
+
53
113
  /**
54
- * Known top-level config keys allowed by the schema.
114
+ * Recursively compare object keys with their TypeBox schema nodes. Root is
115
+ * depth 0; nested objects are traversed through depth 3, which covers the
116
+ * deepest current config shapes (`goalWrap.<workflow>.verification` and
117
+ * `agents.overrides.<agent>`) without allowing attacker-controlled recursion.
55
118
  */
56
- const SCHEMA_KEYS: ReadonlySet<string> = new Set<string>(Object.keys(PiTeamsConfigSchema.properties ?? {}));
119
+ function findUnknownConfigKeyPaths(value: unknown, schema: unknown, depth = 0, path: readonly string[] = []): string[][] {
120
+ if (!isRecord(value) || depth > MAX_CONFIG_KEY_DEPTH) return [];
121
+ const unknownPaths: string[][] = [];
122
+ for (const [key, childValue] of Object.entries(value)) {
123
+ const childPath = [...path, key];
124
+ const resolution = resolveSchemaKey(schema, key);
125
+ if (!resolution.allowed) {
126
+ unknownPaths.push(childPath);
127
+ continue;
128
+ }
129
+ if (resolution.childSchema !== undefined && depth < MAX_CONFIG_KEY_DEPTH) {
130
+ unknownPaths.push(...findUnknownConfigKeyPaths(childValue, resolution.childSchema, depth + 1, childPath));
131
+ }
132
+ }
133
+ return unknownPaths;
134
+ }
135
+
136
+ function isAgentOverrideKeyPath(path: readonly string[]): boolean {
137
+ return path.length === 4 && path[0] === "agents" && path[1] === "overrides";
138
+ }
57
139
 
58
140
  /**
59
141
  * Extract all agent, team, and workflow names referenced in a raw config object.
@@ -94,7 +176,7 @@ function extractConfigReferences(config: Record<string, unknown>): ConfigReferen
94
176
  * 1. **Missing**: resource referenced in config (e.g. agents.overrides) but not discovered → error
95
177
  * 2. **Extra**: discovered resource not referenced anywhere in config → warning
96
178
  * 3. **Mismatched**: resource exists in both but with structural differences → warning
97
- * 4. **Config keys**: config keys not present in the schema → warning
179
+ * 4. **Config keys**: config keys not present in the schema, recursively through depth 3 → warning
98
180
  *
99
181
  * For agents: compares against `agents.overrides` in config. Discovered agents not in overrides
100
182
  * are NOT considered extra (they may be built-in). Only agents referenced in overrides but not
@@ -140,18 +222,18 @@ export function detectDrift(discovered: DiscoveredResources, config: PiTeamsConf
140
222
  // No extra/missing checks for workflows from config alone.
141
223
 
142
224
  // --- Config-key drift ---
143
- const configKeys = Object.keys(configRaw);
144
- for (const key of configKeys) {
145
- if (!SCHEMA_KEYS.has(key)) {
146
- items.push({
147
- kind: "config-key",
148
- name: key,
149
- status: "extra",
150
- expected: `key not in schema`,
151
- actual: `present in config`,
152
- severity: "warning",
153
- });
154
- }
225
+ for (const keyPath of findUnknownConfigKeyPaths(configRaw, PiTeamsConfigSchema)) {
226
+ // Preserve the existing, more specific agent/mismatched finding for
227
+ // agents.overrides.<agent>.<key> instead of reporting the same key twice.
228
+ if (isAgentOverrideKeyPath(keyPath)) continue;
229
+ items.push({
230
+ kind: "config-key",
231
+ name: keyPath.join("."),
232
+ status: "extra",
233
+ expected: `key not in schema`,
234
+ actual: `present in config`,
235
+ severity: "warning",
236
+ });
155
237
  }
156
238
 
157
239
  // --- Mismatched config-key check ---
@@ -86,11 +86,11 @@ export function parseConfigResilient(raw: unknown): ResilientParseResult {
86
86
  const value = raw[key];
87
87
  const subSchema = (PiTeamsConfigSchema.properties as Record<string, unknown>)[key];
88
88
  if (subSchema && !Value.Check(subSchema as TSchema, value)) {
89
- // Collect the first error for this field
89
+ // Collect ALL errors for this field
90
90
  const fieldErrors = [...Value.Errors(subSchema as TSchema, value)];
91
- if (fieldErrors.length > 0) {
92
- const first = fieldErrors[0] as unknown as Record<string, unknown>;
93
- const msg = typeof first.message === "string" ? first.message : "invalid value";
91
+ for (const err of fieldErrors) {
92
+ const errObj = err as unknown as Record<string, unknown>;
93
+ const msg = typeof errObj.message === "string" ? errObj.message : "invalid value";
94
94
  errors.push({
95
95
  field: key,
96
96
  message: msg,
@@ -15,8 +15,6 @@ export interface PiTeamsAutonomousConfig {
15
15
  preferAsyncForLongTasks?: boolean;
16
16
  allowWorktreeSuggestion?: boolean;
17
17
  magicKeywords?: Record<string, string[]>;
18
- /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
19
- excludeContextBash?: boolean;
20
18
  }
21
19
 
22
20
  export interface CrewLimitsConfig {
@@ -93,11 +93,25 @@ export function registerCleanupHandler(pi: ExtensionAPI): void {
93
93
 
94
94
  async function cleanupChildProcesses(): Promise<void> {
95
95
  const pids = childProcessRegistry.getAllPids();
96
-
96
+ if (pids.length === 0) return;
97
+ // Lazy-import to avoid eagerly pulling the heavy runtime/child-pi chain into the
98
+ // extension load path (check-lazy-imports) AND to avoid a static import cycle
99
+ // (child-pi.ts already imports registerChildProcess from this module).
100
+ // LAZY: defer child-pi.ts load (heavy runtime chain) + avoid static import cycle.
101
+ const { killProcessPid } = await import("../runtime/child-pi.ts");
97
102
  for (const pid of pids) {
103
+ // killProcessPid signals the whole process GROUP (-pid on POSIX, taskkill /T on
104
+ // Windows) and escalates to SIGKILL after HARD_KILL_MS. A bare
105
+ // process.kill(pid, "SIGTERM") only reaches the direct child — grandchildren
106
+ // (bash/MCP the worker spawned) in the child's group survive as orphans, and
107
+ // a child that lags SIGTERM would linger forever with no escalation.
108
+ // NOTE: on graceful session_shutdown (reason=quit/reload) register.ts ALSO runs
109
+ // terminateActiveChildPiProcesses(); both are idempotent for a dying pid. This
110
+ // signal handler (SIGTERM/SIGHUP) is the SOLE teardown path when the Pi process
111
+ // is killed by signal, so it must be robust on its own.
98
112
  try {
99
- process.kill(pid, "SIGTERM");
100
- console.log(`[pi-crew] Sent SIGTERM to child process ${pid}`);
113
+ killProcessPid(pid);
114
+ console.log(`[pi-crew] Sent group SIGTERM (+SIGKILL escalation) to child process ${pid}`);
101
115
  } catch (error: unknown) {
102
116
  // Process may already be dead or not exist
103
117
  const err = error as NodeJS.ErrnoException;
@@ -13,7 +13,7 @@ export const CAPACITY_STATUS_ID = "pi-crew-bar";
13
13
  export const PROVIDER_STATUS_ID = "pi-crew-bar";
14
14
 
15
15
  function resolveHome(): string {
16
- return process.env.PI_TEAMS_HOME?.trim() || process.env.HOME || process.env.USERPROFILE || "";
16
+ return (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim() || process.env.HOME || process.env.USERPROFILE || "";
17
17
  }
18
18
 
19
19
  export function configPath(): string {
@@ -33,18 +33,30 @@ export function hasCrewFontFile(): boolean {
33
33
  return _hasFontFile;
34
34
  }
35
35
 
36
+ let _isWebTerminal: boolean | null = null;
37
+
36
38
  /** Returns true when running in a web-based terminal (gotty, wetty, etc.)
37
39
  * where system fonts are not available for PUA rendering. */
38
40
  export function isWebTerminal(): boolean {
39
- if (process.env.GOTTY || process.env.WEBTERM) return true;
40
- if (process.env.TERM === "dumb") return true;
41
+ if (_isWebTerminal !== null) return _isWebTerminal;
42
+ if (process.env.GOTTY || process.env.WEBTERM) {
43
+ _isWebTerminal = true;
44
+ return true;
45
+ }
46
+ if (process.env.TERM === "dumb") {
47
+ _isWebTerminal = true;
48
+ return true;
49
+ }
41
50
  try {
42
51
  // Check current process and ancestors — gotty is often the grandparent
43
52
  // (gotty → tmux → pi), so /proc/self/cgroup may not contain it.
44
53
  let pid = process.pid;
45
54
  for (let i = 0; i < 6 && pid > 1; i++) {
46
55
  const cgroup = readFileSync(`/proc/${pid}/cgroup`, "utf8");
47
- if (cgroup.includes("gotty") || cgroup.includes("wetty")) return true;
56
+ if (cgroup.includes("gotty") || cgroup.includes("wetty")) {
57
+ _isWebTerminal = true;
58
+ return true;
59
+ }
48
60
  const match = cgroup.match(/\d+:.*:(.*)/);
49
61
  // Read PPid from status
50
62
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
@@ -54,5 +66,6 @@ export function isWebTerminal(): boolean {
54
66
  } catch {
55
67
  // not Linux or /proc not available
56
68
  }
69
+ _isWebTerminal = false;
57
70
  return false;
58
71
  }
@@ -19,7 +19,7 @@ function safeUiCall(scope: string, fn: () => void): void {
19
19
  }
20
20
  }
21
21
 
22
- import { type CrewVibesConfig, loadConfig, PROVIDER_STATUS_ID, saveConfig } from "./config.ts";
22
+ import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
23
23
  import { intervalForSpeed } from "./figures.ts";
24
24
  import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
25
25
  import { clearProviderUsageCache, fetchProviderUsage, type ProviderUsage } from "./provider-usage.ts";
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
- import { resolveContainedPath } from "../utils/safe-paths.ts";
3
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
4
4
  import { withHmacVerification } from "./rpc-hmac.ts";
5
5
  // Lazy-loaded to avoid pulling team-tool.ts (and its entire runtime chain) into module load.
6
6
  import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
@@ -109,7 +109,10 @@ export function resetRpcRateLimit(): void {
109
109
  rpcRunTimestamps.length = 0;
110
110
  }
111
111
 
112
- function isAllowedRpcRunParams(params: TeamToolParamsValue): {
112
+ export function isAllowedRpcRunParams(
113
+ params: TeamToolParamsValue,
114
+ ctx: ExtensionContext,
115
+ ): {
113
116
  ok: boolean;
114
117
  error?: string;
115
118
  } {
@@ -123,10 +126,16 @@ function isAllowedRpcRunParams(params: TeamToolParamsValue): {
123
126
  error: "RPC run requires config.intent (a non-empty intent string)",
124
127
  };
125
128
  }
126
- // SECURITY: Validate cwd is within the project directory if provided.
129
+ // SECURITY: Validate cwd, if provided, resolves WITHIN the project directory (ctx.cwd).
130
+ // Arg order is (baseDir=ctx.cwd, targetPath=params.cwd): the project dir is the container,
131
+ // the caller-supplied cwd is the contained target. (Previously the args were swapped —
132
+ // resolveContainedPath(params.cwd, ".") — which validates that "." is inside params.cwd,
133
+ // trivially true for ANY path, so the check never rejected anything.)
134
+ // NOTE: defense-in-depth — handleTeamTool currently uses ctx.cwd and ignores params.cwd,
135
+ // but a correct guard here prevents a future regression from silently enabling cwd escape.
127
136
  if (params.cwd && typeof params.cwd === "string") {
128
137
  try {
129
- resolveContainedPath(params.cwd, ".");
138
+ resolveRealContainedPath(ctx.cwd, params.cwd);
130
139
  } catch {
131
140
  return {
132
141
  ok: false,
@@ -152,6 +161,10 @@ export function registerPiCrewRpc(
152
161
  getCtx: () => ExtensionContext | undefined,
153
162
  ): PiCrewRpcHandle | undefined {
154
163
  if (!events) return undefined;
164
+ // Note: the prior loud startup warning has been replaced by a soft, opt-out
165
+ // info note emitted lazily by withHmacVerification only when an actual RPC
166
+ // request is processed (see rpc-hmac.ts). This avoids noisy output during
167
+ // every pi load for users who never wire cross-extension RPC.
155
168
  const unsubs = [
156
169
  on(
157
170
  events,
@@ -214,7 +227,7 @@ export function registerPiCrewRpc(
214
227
  } else {
215
228
  params = { action: "run" };
216
229
  }
217
- const permission = isAllowedRpcRunParams(params);
230
+ const permission = isAllowedRpcRunParams(params, ctx);
218
231
  if (!permission.ok) {
219
232
  reply(events, "pi-crew:rpc:run", id, {
220
233
  success: false,
@@ -269,10 +282,14 @@ export function registerPiCrewRpc(
269
282
  }
270
283
  }, "pi-crew:rpc:status"),
271
284
  ),
272
- on(events, "pi-crew:live-control", (raw) => {
273
- const request = parseLiveControlRealtimeMessage(raw);
274
- if (request) publishLiveControlRealtime(request);
275
- }),
285
+ on(
286
+ events,
287
+ "pi-crew:live-control",
288
+ withHmacVerification((raw) => {
289
+ const request = parseLiveControlRealtimeMessage(raw);
290
+ if (request) publishLiveControlRealtime(request);
291
+ }, "pi-crew:live-control"),
292
+ ),
276
293
  on(
277
294
  events,
278
295
  "pi-crew:rpc:live-control",