pi-crew 0.8.7 → 0.8.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { listRecentRuns } from "../run-index.ts";
3
+ import { findRepoRoot } from "../../utils/paths.ts";
3
4
  import { extractSessionId } from "../../utils/session-utils.ts";
4
5
  import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
5
6
 
@@ -69,6 +70,29 @@ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
69
70
  return lines.join("\n");
70
71
  }
71
72
 
73
+ /**
74
+ * Project-scope filter: keep `run` only if it belongs to the SAME repo as
75
+ * `queryCwd` (or is a user-level / legacy run with no repo). This is the
76
+ * version-independent, reliable barrier against cross-project leaks: even
77
+ * when the session-id filter below cannot fire (ctx.sessionId is absent on
78
+ * some pi versions — observed on pi 0.79.6 ExtensionContext), the cwd filter
79
+ * stops another project's in-flight runs (e.g. edge-ai-agent) from bleeding
80
+ * into this project's ambient status or compaction-resume directive.
81
+ *
82
+ * Note: listRecentRuns already scopes its filesystem scan via scopedRunRoots,
83
+ * BUT it ALSO merges the GLOBAL activeRunEntries() registry (the cross-
84
+ * project dashboard view). That global merge is intentional for the
85
+ * dashboard but wrong for "what should THIS project's session do" — hence
86
+ * this filter at the consumption site.
87
+ */
88
+ function isInProjectScope(run: TeamRunManifest, queryCwd: string): boolean {
89
+ const queryRepo = findRepoRoot(queryCwd);
90
+ if (queryRepo === undefined) return true; // viewer not in a repo → user-level view
91
+ const runRepo = typeof run.cwd === "string" && run.cwd.length > 0 ? findRepoRoot(run.cwd) : undefined;
92
+ if (runRepo === undefined) return true; // run is user-level / legacy / not a repo → include
93
+ return runRepo === queryRepo; // same project only
94
+ }
95
+
72
96
  /**
73
97
  * Collect in-flight (non-terminal) crew runs that must be resumable after
74
98
  * compaction. These are runs the agent was actively working on or awaiting.
@@ -88,7 +112,13 @@ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
88
112
  export function collectInFlightRuns(cwd: string, currentSessionId?: string): TeamRunManifest[] {
89
113
  return listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS).filter((run) => {
90
114
  if (!IN_FLIGHT_RUN_STATUSES.has(run.status)) return false;
91
- if (currentSessionId === undefined) return true; // no filter → back-compat
115
+ // Reliable barrier (2026-06-17): never leak another project's runs into
116
+ // THIS project's resume directive / ambient status, regardless of
117
+ // whether the session-id filter is available. This fixes the live
118
+ // cross-session leak that persisted after 4bd6f5b because ctx.sessionId
119
+ // is absent on pi 0.79.6.
120
+ if (!isInProjectScope(run, cwd)) return false;
121
+ if (currentSessionId === undefined) return true; // no session filter → back-compat (still project-scoped)
92
122
  return run.ownerSessionId === currentSessionId; // strict: legacy ownerless runs excluded
93
123
  });
94
124
  }
@@ -76,7 +76,7 @@ function scopedRunRoots(cwd: string): string[] {
76
76
  return [...roots];
77
77
  }
78
78
 
79
- function collectActiveRuns(): TeamRunManifest[] {
79
+ function collectActiveRuns(cwd?: string): TeamRunManifest[] {
80
80
  return activeRunEntries()
81
81
  .map((entry) => readManifest(entry.manifestPath))
82
82
  .filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
@@ -29,6 +29,31 @@ import { appendEventAsync, readEvents } from "../../state/event-log.ts";
29
29
  import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtime-resolver.ts";
30
30
  import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
31
31
  import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
32
+
33
+ /**
34
+ * Module-scoped latch for the crew-init dynamic import.
35
+ *
36
+ * `crew-init.ts` is dynamically `await import()`'d from `handleRun` below, which
37
+ * N concurrent subagents hit simultaneously (every `team` tool call runs it).
38
+ * Under the tsx/jiti loader, concurrent first-imports race module-record
39
+ * instantiation → top-level `const` initializers (e.g. CREW_README) hit TDZ
40
+ * (`Cannot access 'CREW_README' before initialization`) and namespace bindings
41
+ * arrive as `undefined` (`reading 'existsSync'`). crew-init.ts's own header
42
+ * documents this for the `path` binding; the race persists for other top-level
43
+ * consts because module-body evaluation itself races.
44
+ *
45
+ * The latch makes concurrent callers share ONE in-flight import promise, so the
46
+ * module body evaluates exactly once regardless of fanout. Same pattern as
47
+ * runtime-warmup.ts / the v0.8.1 peer-dep latch, applied to this specific
48
+ * dynamic-import race site.
49
+ */
50
+ let crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
51
+ function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
52
+ if (!crewInitPromise) {
53
+ crewInitPromise = import("../../state/crew-init.ts");
54
+ }
55
+ return crewInitPromise;
56
+ }
32
57
  import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
33
58
  import { waitForRun } from "../../runtime/run-tracker.ts";
34
59
  import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
@@ -81,9 +106,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
81
106
  const intentPrefix = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
82
107
 
83
108
  // P0: Ensure .crew directory structure exists before creating any manifests.
84
- // Dynamic import to avoid module binding issues in child-process contexts.
109
+ // Latched dynamic import (loadCrewInit) concurrent `team` tool calls from
110
+ // N subagents share ONE in-flight promise so crew-init.ts's module body
111
+ // evaluates exactly once (avoids the cold-start race on CREW_README / path / fs).
85
112
  const workingDir = ctx.cwd ?? process.cwd();
86
- const { ensureCrewDirectory } = await import("../../state/crew-init.ts");
113
+ const { ensureCrewDirectory } = await loadCrewInit();
87
114
  await ensureCrewDirectory(workingDir);
88
115
 
89
116
  // WORKTREE FIX: If worktree mode is needed but cwd is not a git repo,
@@ -54,6 +54,7 @@ const HOT_MODULE_SPECIFIERS = [
54
54
  "./task-runner.ts",
55
55
  "../extension/team-tool.ts",
56
56
  "../extension/validate-resources.ts",
57
+ "../state/crew-init.ts", // TDZ-prone top-level consts (CREW_README); dynamically imported by team-tool/run.ts
57
58
  ] as const;
58
59
 
59
60
  /** Additional bare-specifier peer deps to warm. */