pi-crew 0.7.7 → 0.8.2

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.
@@ -21,6 +21,8 @@ const PACKAGE_SKILLS_DIR = path.resolve(
21
21
  "..",
22
22
  "skills",
23
23
  );
24
+ import * as os from "node:os";
25
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
26
  const MAX_SKILL_CHARS = 1500;
25
27
  const MAX_TOTAL_CHARS = 6000;
26
28
  const MAX_SKILL_NAME_CHARS = 80;
@@ -139,16 +141,24 @@ export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
139
141
 
140
142
  function candidateSkillDirs(
141
143
  cwd: string,
142
- ): Array<{ root: string; source: "project" | "package" }> {
144
+ ): Array<{ root: string; source: "project" | "package" | "project-pi" | "user-pi" | "project-agents" | "user-agents" }> {
143
145
  return [
144
146
  { root: PACKAGE_SKILLS_DIR, source: "package" }, // ✓ Trusted first
145
- { root: path.resolve(cwd, "skills"), source: "project" }, // ⚠️ Override second
147
+ // F6 (v0.7.9): same five roots as discover-skills, in the same precedence
148
+ // order. The first hit wins, so a project `.pi/skills/foo/SKILL.md`
149
+ // overrides both the bundled `foo` and any legacy `<cwd>/skills/foo`.
150
+ { root: path.resolve(cwd, ".pi", "skills"), source: "project-pi" },
151
+ { root: path.resolve(cwd, ".agents", "skills"), source: "project-agents" },
152
+ { root: path.resolve(cwd, "skills"), source: "project" },
153
+ { root: path.join(getAgentDir(), "skills"), source: "user-pi" },
154
+ { root: path.join(os.homedir(), ".agents", "skills"), source: "user-agents" },
155
+ { root: path.join(os.homedir(), ".pi", "skills"), source: "user-pi" },
146
156
  ];
147
157
  }
148
158
 
149
159
  interface CachedSkillMarkdown {
150
160
  path: string;
151
- source: "project" | "package";
161
+ source: "project" | "package" | "project-pi" | "user-pi" | "project-agents" | "user-agents";
152
162
  content: string;
153
163
  mtimeMs: number;
154
164
  size: number;
@@ -187,7 +197,7 @@ function readSkillMarkdown(
187
197
  cwd: string,
188
198
  name: string,
189
199
  ):
190
- | { path: string; source: "project" | "package"; content: string }
200
+ | { path: string; source: "project" | "package" | "project-pi" | "user-pi" | "project-agents" | "user-agents"; content: string }
191
201
  | undefined {
192
202
  if (!isValidSkillName(name)) return undefined;
193
203
  const cacheKey = `${path.resolve(cwd)}:${name}`;
@@ -32,6 +32,8 @@ import {
32
32
  isRetryableModelFailure,
33
33
  type ModelAttemptSummary,
34
34
  } from "./model-fallback.ts";
35
+ import { readEnabledModelsPatterns } from "./model-scope.ts";
36
+ import { loadConfig } from "../config/config.ts";
35
37
  import { tailReadWithLineSnap } from "./task-runner/tail-read.ts";
36
38
  import {
37
39
  parsePiJsonOutput,
@@ -385,6 +387,7 @@ export async function runTeamTask(
385
387
  parentModel: input.parentModel,
386
388
  modelRegistry: input.modelRegistry,
387
389
  cwd: task.cwd,
390
+ scopeModelsPatterns: await resolveTaskScopeModelsPatterns(task.cwd),
388
391
  });
389
392
  const candidates = modelRoutingPlan.candidates;
390
393
  const attemptModels =
@@ -1306,3 +1309,21 @@ export async function runTeamTask(
1306
1309
  streamBridge?.dispose();
1307
1310
  }
1308
1311
  }
1312
+
1313
+ /**
1314
+ * F7: resolve the enabledModels allowlist for the child-process spawn path,
1315
+ * but only if `runtime.reliability.scopeModels` is ON. Returns [] (no-op)
1316
+ * when the toggle is off or the allowlist is empty. Best-effort: any failure
1317
+ * to read config or the allowlist silently disables the gate so spawn is
1318
+ * never blocked by a misconfiguration.
1319
+ */
1320
+ async function resolveTaskScopeModelsPatterns(cwd: string): Promise<string[]> {
1321
+ let scopeModels = false;
1322
+ try {
1323
+ scopeModels = loadConfig(cwd).config.reliability?.scopeModels === true;
1324
+ } catch {
1325
+ return [];
1326
+ }
1327
+ if (!scopeModels) return [];
1328
+ return readEnabledModelsPatterns(cwd);
1329
+ }
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
5
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
6
  import { logInternalError } from "../utils/internal-error.ts";
5
7
  import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
6
8
 
@@ -12,13 +14,40 @@ let cache: { skills: SkillDescriptor[]; cachedAt: number; cwd: string } | null =
12
14
  export interface SkillDescriptor {
13
15
  name: string;
14
16
  description: string;
15
- source: "project" | "package";
17
+ /**
18
+ * Source of the skill. F6 (v0.7.9) adds the Agent Skills spec roots:
19
+ * - `project-pi` / `user-pi` — Pi's standard `.pi/skills/`
20
+ * - `project-agents` / `user-agents` — cross-tool Agent Skills spec (`.agents/skills/`)
21
+ * The original `project` / `package` are kept for back-compat.
22
+ */
23
+ source: "project" | "package" | "project-pi" | "user-pi" | "project-agents" | "user-agents";
16
24
  path: string;
17
25
  }
18
26
 
19
- function listSkillDirs(cwd: string): Array<{ root: string; source: "project" | "package" }> {
27
+ /**
28
+ * F6 (v0.7.9): discover skills from all five roots (matching pi-subagents'
29
+ * skill-loader so users authoring skills under either convention find them).
30
+ * Roots, in precedence order (first hit wins):
31
+ * 1. <cwd>/.pi/skills (project, Pi standard)
32
+ * 2. <cwd>/.agents/skills (project, Agent Skills spec — agentskills.io)
33
+ * 3. <cwd>/skills (project, legacy pi-crew convention)
34
+ * 4. <getAgentDir>/skills (user, Pi standard)
35
+ * 5. <homedir>/.agents/skills (user, Agent Skills spec)
36
+ * 6. <homedir>/.pi/skills (user, legacy Pi — pre-standard)
37
+ * 7. PACKAGE_SKILLS_DIR (bundled, trusted)
38
+ * The `PACKAGE_SKILLS_DIR` (bundled) and the legacy `<cwd>/skills` root are
39
+ * kept as separate `source` values to preserve the existing capability
40
+ * inventory shape — callers that key on `source === "package"` / `source ===
41
+ * "project"` keep working.
42
+ */
43
+ function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescriptor["source"] }> {
20
44
  return [
45
+ { root: path.resolve(cwd, ".pi", "skills"), source: "project-pi" },
46
+ { root: path.resolve(cwd, ".agents", "skills"), source: "project-agents" },
21
47
  { root: path.resolve(cwd, "skills"), source: "project" },
48
+ { root: path.join(getAgentDir(), "skills"), source: "user-pi" },
49
+ { root: path.join(os.homedir(), ".agents", "skills"), source: "user-agents" },
50
+ { root: path.join(os.homedir(), ".pi", "skills"), source: "user-pi" },
22
51
  { root: PACKAGE_SKILLS_DIR, source: "package" },
23
52
  ];
24
53
  }
@@ -65,7 +65,7 @@ export interface AgentOverlayState {
65
65
  export function createAgentOverlayState(entries: AgentEntry[], maxVisible = 20): AgentOverlayState {
66
66
  return {
67
67
  entries: entries.sort((a, b) => {
68
- const order: Record<ResourceSource, number> = { project: 0, user: 1, git: 2, builtin: 3, dynamic: 4 };
68
+ const order: Record<ResourceSource, number> = { project: 0, "project-pi": 1, user: 2, git: 3, builtin: 4, dynamic: 5 };
69
69
  const diff = (order[a.source] ?? 4) - (order[b.source] ?? 4);
70
70
  return diff !== 0 ? diff : a.name.localeCompare(b.name);
71
71
  }),
@@ -49,4 +49,34 @@ export function safeToPiSessionId(runId: string): string | undefined {
49
49
  } catch {
50
50
  return undefined;
51
51
  }
52
+ }
53
+
54
+ /**
55
+ * Extract the current Pi session id from an ExtensionContext.
56
+ *
57
+ * `ExtensionContext` does not declare `sessionId` in its type, but the runtime
58
+ * attaches it as an own property. We read it via `getOwnPropertyDescriptor`
59
+ * to safely bypass any Proxy traps, then validate it as a non-empty string.
60
+ *
61
+ * This is the canonical accessor — every site that filters the SHARED
62
+ * per-project `.crew/state/` tree down to the current session MUST use this,
63
+ * otherwise cross-session state leaks (e.g. compaction-guard resuming another
64
+ * session's runs, ambient-status injecting another session's runs).
65
+ *
66
+ * Returns undefined when the session id is absent or unparseable — callers
67
+ * must decide whether to treat that as "no filter" (back-compat) or "no runs".
68
+ */
69
+ export function extractSessionId(ctx: unknown): string | undefined {
70
+ if (typeof ctx !== "object" || ctx === null) return undefined;
71
+ let raw: unknown;
72
+ try {
73
+ raw = Object.getOwnPropertyDescriptor(ctx, "sessionId")?.value;
74
+ } catch {
75
+ // Defensive: a hostile Proxy or exotic object may trap descriptor
76
+ // access. Real Pi ExtensionContext objects are plain, so this is
77
+ // only hit by adversarial/degenerate inputs — treat as no session id.
78
+ return undefined;
79
+ }
80
+ if (typeof raw !== "string" || raw.length === 0) return undefined;
81
+ return raw;
52
82
  }