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.
- package/CHANGELOG.md +49 -0
- package/dist/index.mjs +22609 -21373
- package/package.json +1 -1
- package/skills/distill-software/SKILL.md +7 -1
- package/src/agents/discover-agents.ts +6 -0
- package/src/config/config.ts +13 -2
- package/src/config/markers.ts +9 -1
- package/src/extension/action-suggestions.ts +1 -1
- package/src/extension/async-notifier.ts +13 -5
- package/src/extension/registration/context-builder.ts +5 -2
- package/src/extension/registration/crash-recovery-cache.ts +2 -2
- package/src/extension/registration/lazy-configurers.ts +1 -1
- package/src/extension/registration/lifecycle-handlers.ts +25 -10
- package/src/extension/registration/observability.ts +8 -4
- package/src/extension/registration/registration-types.ts +2 -2
- package/src/extension/registration/runtime-cleanup.ts +10 -2
- package/src/extension/registration/subagent-manager-setup.ts +9 -4
- package/src/extension/registration/subagent-tools.ts +15 -3
- package/src/extension/run-import.ts +19 -2
- package/src/extension/run-maintenance.ts +25 -9
- package/src/extension/session-summary.ts +5 -0
- package/src/extension/team-tool/destructive-gate.ts +12 -6
- package/src/extension/team-tool/health-monitor.ts +7 -5
- package/src/extension/team-tool/intent-policy.ts +9 -0
- package/src/extension/team-tool/lifecycle-actions.ts +27 -4
- package/src/extension/team-tool/run-deadline.ts +7 -2
- package/src/extension/team-tool/run.ts +23 -4
- package/src/extension/team-tool/status.ts +2 -2
- package/src/extension/team-tool.ts +126 -51
- package/src/runtime/background-runner.ts +36 -2
- package/src/runtime/delivery-coordinator.ts +24 -3
- package/src/runtime/foreground-watchdog.ts +2 -2
- package/src/runtime/model/model-fallback.ts +3 -1
- package/src/runtime/model/provider-extensions.ts +36 -0
- package/src/runtime/model/runtime-warmup.ts +41 -0
- package/src/runtime/peer-dep.ts +35 -8
- package/src/runtime/recovery/crash-recovery.ts +34 -4
- package/src/runtime/skill-instructions.ts +1 -1
- package/src/runtime/stale-reconciler.ts +11 -19
- package/src/runtime/subagent-manager.ts +15 -6
- package/src/runtime/task-packet.ts +26 -4
- package/src/runtime/task-runner/run-projection.ts +4 -1
- package/src/runtime/team-runner.ts +94 -72
- package/src/runtime/verification/completion-guard.ts +10 -1
- package/src/schema/team-tool-schema.ts +152 -38
- package/src/skills/validate.ts +28 -2
- package/src/state/atomic-write.ts +23 -12
- package/src/state/contracts.ts +1 -0
- package/src/state/coordination/locks.ts +20 -7
- package/src/state/coordination/mailbox.ts +20 -2
- package/src/state/gitignore-manager.ts +5 -1
- package/src/state/stores/artifact-store.ts +5 -5
- package/src/state/stores/run-cache.ts +9 -1
- package/src/state/stores/state-store.ts +64 -10
- package/src/ui/deploy-bundled-themes.ts +11 -0
- package/src/ui/powerbar-publisher.ts +31 -6
- package/src/ui/run-dashboard.ts +7 -1
- package/src/ui/syntax-highlight.ts +31 -12
- package/src/ui/widget/index.ts +9 -1
- package/src/ui/widget/widget-model.ts +1 -1
- package/src/ui/widget/widget-types.ts +3 -0
- package/src/utils/env-filter.ts +25 -11
- package/src/utils/paths.ts +20 -1
- package/src/utils/redaction.ts +22 -1
- package/src/utils/session-utils.ts +42 -19
- package/src/worktree/worktree-manager.ts +2 -2
package/src/utils/paths.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/utils/redaction.ts
CHANGED
|
@@ -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`
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
|
|
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
|
|
74
|
-
// access. Real Pi ExtensionContext objects are plain, so
|
|
75
|
-
// only hit by adversarial/degenerate inputs — treat as no
|
|
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
|
-
|
|
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
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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) {
|