pi-crew 0.9.57 → 0.9.59

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 (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/index.mjs +22609 -21373
  3. package/package.json +1 -1
  4. package/skills/distill-software/SKILL.md +7 -1
  5. package/src/agents/discover-agents.ts +6 -0
  6. package/src/config/config.ts +13 -2
  7. package/src/config/markers.ts +9 -1
  8. package/src/extension/action-suggestions.ts +1 -1
  9. package/src/extension/async-notifier.ts +13 -5
  10. package/src/extension/registration/context-builder.ts +5 -2
  11. package/src/extension/registration/crash-recovery-cache.ts +2 -2
  12. package/src/extension/registration/lazy-configurers.ts +1 -1
  13. package/src/extension/registration/lifecycle-handlers.ts +25 -10
  14. package/src/extension/registration/observability.ts +8 -4
  15. package/src/extension/registration/registration-types.ts +2 -2
  16. package/src/extension/registration/runtime-cleanup.ts +10 -2
  17. package/src/extension/registration/subagent-manager-setup.ts +9 -4
  18. package/src/extension/registration/subagent-tools.ts +15 -3
  19. package/src/extension/run-import.ts +19 -2
  20. package/src/extension/run-maintenance.ts +25 -9
  21. package/src/extension/session-summary.ts +5 -0
  22. package/src/extension/team-tool/destructive-gate.ts +12 -6
  23. package/src/extension/team-tool/health-monitor.ts +7 -5
  24. package/src/extension/team-tool/intent-policy.ts +9 -0
  25. package/src/extension/team-tool/lifecycle-actions.ts +27 -4
  26. package/src/extension/team-tool/run-deadline.ts +7 -2
  27. package/src/extension/team-tool/run.ts +23 -4
  28. package/src/extension/team-tool/status.ts +2 -2
  29. package/src/extension/team-tool.ts +126 -51
  30. package/src/runtime/background-runner.ts +36 -2
  31. package/src/runtime/delivery-coordinator.ts +24 -3
  32. package/src/runtime/foreground-watchdog.ts +2 -2
  33. package/src/runtime/model/model-fallback.ts +3 -1
  34. package/src/runtime/model/provider-extensions.ts +36 -0
  35. package/src/runtime/model/runtime-warmup.ts +41 -0
  36. package/src/runtime/peer-dep.ts +35 -8
  37. package/src/runtime/recovery/crash-recovery.ts +34 -4
  38. package/src/runtime/skill-instructions.ts +1 -1
  39. package/src/runtime/stale-reconciler.ts +11 -19
  40. package/src/runtime/subagent-manager.ts +15 -6
  41. package/src/runtime/task-packet.ts +26 -4
  42. package/src/runtime/task-runner/run-projection.ts +4 -1
  43. package/src/runtime/team-runner.ts +94 -72
  44. package/src/runtime/verification/completion-guard.ts +10 -1
  45. package/src/schema/team-tool-schema.ts +152 -38
  46. package/src/skills/validate.ts +28 -2
  47. package/src/state/atomic-write.ts +23 -12
  48. package/src/state/contracts.ts +1 -0
  49. package/src/state/coordination/locks.ts +20 -7
  50. package/src/state/coordination/mailbox.ts +20 -2
  51. package/src/state/gitignore-manager.ts +5 -1
  52. package/src/state/stores/artifact-store.ts +5 -5
  53. package/src/state/stores/run-cache.ts +9 -1
  54. package/src/state/stores/state-store.ts +64 -10
  55. package/src/ui/deploy-bundled-themes.ts +11 -0
  56. package/src/ui/powerbar-publisher.ts +31 -6
  57. package/src/ui/run-dashboard.ts +7 -1
  58. package/src/ui/syntax-highlight.ts +31 -12
  59. package/src/ui/widget/index.ts +9 -1
  60. package/src/ui/widget/widget-model.ts +1 -1
  61. package/src/ui/widget/widget-types.ts +3 -0
  62. package/src/utils/env-filter.ts +25 -11
  63. package/src/utils/paths.ts +20 -1
  64. package/src/utils/redaction.ts +22 -1
  65. package/src/utils/session-utils.ts +42 -19
  66. package/src/worktree/worktree-manager.ts +2 -2
@@ -3,7 +3,14 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
+ // NEW-P1/NEW-P2 (perf): packageRoot() and userPiRoot() are invariant for a
7
+ // process lifetime but each call did statSync/readFileSync/lstatSync (10-15+
8
+ // call sites, including at MODULE-LOAD time). Memoize both: compute on first
9
+ // call, return the cached value thereafter. Resolution logic is unchanged.
10
+ let cachedPackageRoot: string | undefined;
11
+
6
12
  export function packageRoot(): string {
13
+ if (cachedPackageRoot !== undefined) return cachedPackageRoot;
7
14
  // Phase 5 H2 follow-up: this function is called from both src/ paths
8
15
  // (e.g. src/utils/paths.ts) AND from dist/index.mjs after bundling.
9
16
  // The original 2-level walk only worked for src/ paths:
@@ -22,6 +29,7 @@ export function packageRoot(): string {
22
29
  try {
23
30
  const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8")) as { name?: string };
24
31
  if (pkg.name === "pi-crew") {
32
+ cachedPackageRoot = dir;
25
33
  return dir;
26
34
  }
27
35
  } catch {
@@ -35,9 +43,12 @@ export function packageRoot(): string {
35
43
  // Fallback: 2-level walk (matches original src/ semantics). Bundles
36
44
  // that don't ship a package.json (very unusual) will get a wrong path
37
45
  // here, but that's a build-pipeline bug we want to surface, not hide.
38
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
46
+ cachedPackageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
47
+ return cachedPackageRoot;
39
48
  }
40
49
 
50
+ let cachedUserPiRoot: { home: string; value: string } | undefined;
51
+
41
52
  export function userPiRoot(): string {
42
53
  // F4: a misconfigured PI_TEAMS_HOME can be the literal string "undefined"
43
54
  // (e.g. PI_TEAMS_HOME=$UNSET_VAR in a shell), which would build paths like
@@ -46,6 +57,12 @@ export function userPiRoot(): string {
46
57
  // os.homedir().
47
58
  const rawHome = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
48
59
  const home = rawHome && rawHome !== "undefined" ? rawHome : os.homedir();
60
+ // NEW-P2: memoize but KEY ON HOME. Tests (withIsolatedHome / isolateHome)
61
+ // change PI_TEAMS_HOME mid-process; an unkeyed cache would leak the first
62
+ // test's root across the suite (broken isolation — surfaced by active-run-
63
+ // registry / health-monitor run counts). In production home is stable, so
64
+ // this still collapses ~15 lstatSync call sites to one stat per distinct home.
65
+ if (cachedUserPiRoot?.home === home) return cachedUserPiRoot.value;
49
66
  const resolved = path.join(home, ".pi", "agent");
50
67
 
51
68
  // Reject symlinks to prevent confusion attacks where PI_TEAMS_HOME points to
@@ -59,6 +76,7 @@ export function userPiRoot(): string {
59
76
  } catch (err: unknown) {
60
77
  if (err instanceof Error && "code" in err && err.code !== "ENOENT") throw err;
61
78
  // Path doesn't exist yet — caller will create it. Skip further validation.
79
+ cachedUserPiRoot = { home, value: resolved };
62
80
  return resolved;
63
81
  }
64
82
  if (isSymlink) {
@@ -88,6 +106,7 @@ export function userPiRoot(): string {
88
106
  // (race condition). This is acceptable — caller will handle.
89
107
  }
90
108
 
109
+ cachedUserPiRoot = { home, value: resolved };
91
110
  return resolved;
92
111
  }
93
112
 
@@ -63,6 +63,17 @@ export const SLACK_TOKEN_PATTERN = /(?<![A-Za-z0-9_-])xox[baprs]-[A-Za-z0-9-]{10
63
63
  export const GOOGLE_API_KEY_PATTERN = /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g;
64
64
  export const STRIPE_KEY_PATTERN = /(?<![A-Za-z0-9_])sk_live_[0-9a-zA-Z]{24}(?![0-9a-zA-Z])/g;
65
65
 
66
+ // DI-2: Anthropic + OpenAI API key value formats. These mirror the same
67
+ // ReDoS-safe shape as the OQ13 set: a single bounded {20,} quantifier on a
68
+ // plain char class, anchored by a negative lookbehind (no nested quantifiers,
69
+ // no alternation). The lookbehind prevents matching inside a longer identifier
70
+ // (e.g. "mysk-ant-api03..."), and the 20+ char tail keeps short "sk-"
71
+ // prefixes (a common legit abbreviation) from being over-redacted. NOTE: the
72
+ // lookbehind is a fixed-width zero-width assertion — linear, safe.
73
+ export const ANTHROPIC_KEY_PATTERN = /(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{20,}/g;
74
+ export const OPENAI_PROJECT_KEY_PATTERN = /(?<![A-Za-z0-9_-])sk-proj-[A-Za-z0-9_-]{20,}/g;
75
+ export const OPENAI_KEY_PATTERN = /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{20,}/g;
76
+
66
77
  // Linear-time secret key detection
67
78
  // IMPORTANT: This function must maintain linear-time guarantees.
68
79
  // The fast-path regex uses simple string alternatives with anchors only (no quantifiers),
@@ -236,6 +247,12 @@ const SECRET_MARKERS_CASE_SENSITIVE = [
236
247
  "AIza",
237
248
  "sk_live_",
238
249
  "xox",
250
+ // DI-2: Anthropic (sk-ant-) and OpenAI (sk-proj-) API key prefixes. The
251
+ // bare "sk-" prefix is NOT a marker (too common as a short word/abbrev and
252
+ // would defeat the pre-filter skip); OPENAI_KEY_PATTERN's {20,} tail still
253
+ // catches bare sk-... keys via the value-pattern pass.
254
+ "sk-ant-",
255
+ "sk-proj-",
239
256
  // GitHub PAT prefixes (gh[pousr]_) — exact, not the over-broad "gh" which
240
257
  // matched "though"/"highlight"/"might" and defeated the pre-filter skip.
241
258
  "ghp_",
@@ -296,7 +313,11 @@ export function redactSecretString(value: string): string {
296
313
  .replace(AWS_ACCESS_KEY_PATTERN, "***")
297
314
  .replace(SLACK_TOKEN_PATTERN, "***")
298
315
  .replace(GOOGLE_API_KEY_PATTERN, "***")
299
- .replace(STRIPE_KEY_PATTERN, "***");
316
+ .replace(STRIPE_KEY_PATTERN, "***")
317
+ // DI-2: Anthropic + OpenAI API keys (value-based formats).
318
+ .replace(ANTHROPIC_KEY_PATTERN, "***")
319
+ .replace(OPENAI_PROJECT_KEY_PATTERN, "***")
320
+ .replace(OPENAI_KEY_PATTERN, "***");
300
321
 
301
322
  // Replace inline secrets: key=value or key:value patterns
302
323
  result = redactInlineSecrets(result);
@@ -49,34 +49,57 @@ export function safeToPiSessionId(runId: string): string | undefined {
49
49
  }
50
50
  }
51
51
 
52
+ /**
53
+ * Module-level cache for resolved session ids, keyed by the stable
54
+ * `sessionManager` reference (NOT by `ctx` — `ctx` is recreated per event by
55
+ * `createContext()` on the runner, so keying on `ctx` would never hit the
56
+ * cache). The `sessionManager` object persists across events within a session.
57
+ */
58
+ const sessionIdCache = new WeakMap<object, string>();
59
+
52
60
  /**
53
61
  * Extract the current Pi session id from an ExtensionContext.
54
62
  *
55
- * `ExtensionContext` does not declare `sessionId` in its type, but the runtime
56
- * attaches it as an own property. We read it via `getOwnPropertyDescriptor`
57
- * to safely bypass any Proxy traps, then validate it as a non-empty string.
63
+ * On Pi 0.83.0 the `ExtensionContext` has NO top-level `sessionId` property —
64
+ * the id is reachable only via `ctx.sessionManager.getSessionId()`. This is
65
+ * the canonical accessor: every site that filters the SHARED per-project
66
+ * `.crew/state/` tree down to the current session MUST use this, otherwise
67
+ * cross-session state leaks (e.g. compaction-guard resuming another session's
68
+ * runs, ambient-status injecting another session's runs).
58
69
  *
59
- * This is the canonical accessor — every site that filters the SHARED
60
- * per-project `.crew/state/` tree down to the current session MUST use this,
61
- * otherwise cross-session state leaks (e.g. compaction-guard resuming another
62
- * session's runs, ambient-status injecting another session's runs).
70
+ * Strategy (primary → fallback):
71
+ * 1. `ctx.sessionManager?.getSessionId?.()` — the working accessor on
72
+ * Pi 0.83.0. The result is cached by `sessionManager` ref so the method
73
+ * lookup runs at most once per session-manager instance, keeping the hot
74
+ * path (called on every `context` event) cheap.
75
+ * 2. `Object.getOwnPropertyDescriptor(ctx, "sessionId")` — for test mocks
76
+ * and older Pi versions that attach `sessionId` as an own property.
63
77
  *
64
78
  * Returns undefined when the session id is absent or unparseable — callers
65
79
  * must decide whether to treat that as "no filter" (back-compat) or "no runs".
66
80
  */
67
81
  export function extractSessionId(ctx: unknown): string | undefined {
68
82
  if (typeof ctx !== "object" || ctx === null) return undefined;
69
- let raw: unknown;
70
83
  try {
71
- raw = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
84
+ const sm = (ctx as { sessionManager?: { getSessionId?: () => unknown } }).sessionManager;
85
+ if (sm && typeof sm === "object") {
86
+ const cached = sessionIdCache.get(sm as object);
87
+ if (cached) return cached;
88
+ const id = (sm as { getSessionId?: () => unknown }).getSessionId?.();
89
+ if (typeof id === "string" && id.length > 0) {
90
+ sessionIdCache.set(sm as object, id);
91
+ return id;
92
+ }
93
+ }
94
+ const direct = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
95
+ if (typeof direct === "string" && direct.length > 0) return direct;
72
96
  } catch {
73
- // Defensive: a hostile Proxy or exotic object may trap descriptor
74
- // access. Real Pi ExtensionContext objects are plain, so this is
75
- // only hit by adversarial/degenerate inputs — treat as no session id.
97
+ // Defensive: a hostile Proxy or exotic object may trap property or
98
+ // descriptor access. Real Pi ExtensionContext objects are plain, so
99
+ // this is only hit by adversarial/degenerate inputs — treat as no id.
76
100
  return undefined;
77
101
  }
78
- if (typeof raw !== "string" || raw.length === 0) return undefined;
79
- return raw;
102
+ return undefined;
80
103
  }
81
104
 
82
105
  /**
@@ -86,11 +109,11 @@ export function extractSessionId(ctx: unknown): string | undefined {
86
109
  * its public surface — the id is reachable via `ctx.sessionManager.getSessionId()`.
87
110
  * This helper is only called from `installCrewBrokerLifecycleController.setSessionId`
88
111
  * (once per session_start), so the extra method invocation is safe here. It is
89
- * INTENTIONALLY a separate function from `extractSessionId`, which is called on
90
- * every `context` event (before every LLM call) from `context-status-injection.ts`
91
- * — extending that hot path with method calls was observed to freeze the TUI
92
- * (dashboard opens but is unresponsive, footer does not render) during smoke
93
- * testing, so it stays on the trivial property lookup.
112
+ * INTENTIONALLY a separate function from `extractSessionId`: both now read
113
+ * `sessionManager.getSessionId()` first, but `extractSessionId` memoises the
114
+ * result in a WeakMap keyed by the `sessionManager` ref (it is called on every
115
+ * `context` event from `context-status-injection.ts`), whereas this broker
116
+ * helper is called only once per `session_start`, so caching is unnecessary.
94
117
  *
95
118
  * Tries the sessionManager path first, then falls back to a direct
96
119
  * `ctx.sessionId` for test mock compatibility.
@@ -767,7 +767,7 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
767
767
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
768
768
  }
769
769
  // ST-1: use -uall so untracked directories are expanded to individual files.
770
- const dirtyStatus = git(worktreePath, ["status", "--porcelain", "-uall"]);
770
+ const dirtyStatus = git(worktreePath, ["-c", "core.quotePath=false", "status", "--porcelain", "-uall"]);
771
771
  if (dirtyStatus.trim()) {
772
772
  // Snapshot uncommitted work to a recovery artifact BEFORE discarding, so the
773
773
  // previous run's changes are never silently destroyed on reuse.
@@ -927,7 +927,7 @@ export async function prepareTaskWorkspaceAsync(
927
927
  throw new Error(`Existing worktree branch mismatch at ${worktreePath}: expected '${branch}', got '${currentBranch}'.`);
928
928
  }
929
929
  // ST-1: use -uall so untracked directories are expanded to individual files.
930
- const dirtyStatus = await gitAsync(worktreePath, ["status", "--porcelain", "-uall"]);
930
+ const dirtyStatus = await gitAsync(worktreePath, ["-c", "core.quotePath=false", "status", "--porcelain", "-uall"]);
931
931
  if (dirtyStatus.trim()) {
932
932
  const snapshotOk = snapshotDirtyWorktree(manifest, task, worktreePath, dirtyStatus);
933
933
  if (snapshotOk) {