@tintinweb/pi-subagents 0.7.3 → 0.9.0

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 (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +22 -1
  3. package/dist/agent-manager.d.ts +2 -2
  4. package/dist/agent-runner.d.ts +3 -3
  5. package/dist/agent-runner.js +1 -1
  6. package/dist/context.d.ts +1 -1
  7. package/dist/custom-agents.js +1 -1
  8. package/dist/default-agents.js +3 -3
  9. package/dist/enabled-models.d.ts +49 -0
  10. package/dist/enabled-models.js +145 -0
  11. package/dist/env.d.ts +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +215 -84
  14. package/dist/output-file.d.ts +1 -1
  15. package/dist/schedule-store.d.ts +2 -0
  16. package/dist/schedule-store.js +12 -1
  17. package/dist/schedule.d.ts +1 -1
  18. package/dist/settings.d.ts +23 -0
  19. package/dist/settings.js +6 -1
  20. package/dist/skill-loader.js +1 -1
  21. package/dist/types.d.ts +2 -2
  22. package/dist/ui/agent-widget.js +1 -1
  23. package/dist/ui/conversation-viewer.d.ts +2 -2
  24. package/dist/ui/conversation-viewer.js +1 -1
  25. package/dist/ui/schedule-menu.d.ts +1 -1
  26. package/package.json +4 -4
  27. package/src/agent-manager.ts +2 -2
  28. package/src/agent-runner.ts +3 -3
  29. package/src/context.ts +1 -1
  30. package/src/custom-agents.ts +1 -1
  31. package/src/default-agents.ts +3 -3
  32. package/src/enabled-models.ts +180 -0
  33. package/src/env.ts +1 -1
  34. package/src/index.ts +238 -85
  35. package/src/output-file.ts +1 -1
  36. package/src/schedule-store.ts +11 -1
  37. package/src/schedule.ts +1 -1
  38. package/src/settings.ts +28 -1
  39. package/src/skill-loader.ts +1 -1
  40. package/src/types.ts +2 -2
  41. package/src/ui/agent-widget.ts +1 -1
  42. package/src/ui/conversation-viewer.ts +2 -2
  43. package/src/ui/schedule-menu.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-05-30
11
+
12
+ > **Heads-up — orchestrator behavior may shift.** This release substantially rewrites the `Agent` tool description and the three default-agent descriptions (`general-purpose`, `Explore`, `Plan`) to mirror Claude Code's upstream wording. No API, schema, or tool-call shape changes — purely a prompt-engineering shift, but a load-bearing one:
13
+ > - **Agent selection may drift.** The new agent descriptions carry richer positive ("Use it to …") and negative ("Do NOT use it for …") guidance plus search-breadth hints for `Explore` (`"quick"` / `"medium"` / `"very thorough"`). For ambiguous tasks where the orchestrator previously picked one default agent, it may now pick another — typically more correctly, but the choice may differ from prior releases.
14
+ > - **Subagent briefings will skew longer and more contextual.** The restored upstream guardrails and the new `## Writing the prompt` section actively coach "smart colleague who just walked into the room"-style prompts. Expect more context, more constraint, more upfront framing in the `prompt:` field the orchestrator passes to subagents.
15
+ > - **Parallel/background patterns more strongly enforced.** The merged bullet on parallel execution now explicitly says `run_in_background: true` is required on each tool call for actual concurrency, and that the orchestrator MUST send a single message with multiple tool uses when the user says "in parallel." Workflows relying on sequential-foreground default behavior are unaffected.
16
+ > - If you have tests or workflows that depend on the prior agent-selection or briefing behavior, pin to a v0.7.x release.
17
+
18
+ ### Added
19
+ - **`scopeModels` setting — opt-in subagent model-scope enforcement** (off by default). New setting toggleable via `/agents → Settings → Scope models`. When enabled, the *effective* model of each subagent spawn is validated against `enabledModels` from pi's settings (which pi manages via its own `/scoped-models` UI; pi-subagents only reads it). **Both pi settings files are honored**: global `<agentDir>/settings.json` plus project-local `<cwd>/.pi/settings.json`, with project overriding global — mirrors pi's `SettingsManager` deep-merge and our own `subagents.json` precedence. Out-of-scope handling depends on source: caller-supplied via `Agent({ model: "..." })` → hard error to the orchestrator with the allowed list; frontmatter-pinned or parent-inherited → warning toast + the agent runs anyway (preserves "frontmatter is authoritative" guarantee from v0.5.1; `scopeModels` is a guardrail against runtime LLM choices, not user-level config). Limitation: only exact `provider/modelId` entries in `enabledModels` are honored — globs (`*sonnet*`), bare model IDs, and `:thinking` suffixes that pi itself supports are silently dropped here. Matches pi's `/scoped-models` picker output, so the limitation is invisible to UI users.
20
+
21
+ ### Changed
22
+ - **`Agent` tool prompt restructured to mirror Claude Code's upstream Agent tool description format.** Section headings now match upstream (`## When not to use`, `## Usage notes`, `## Writing the prompt`); the auto-generated agent list renders as a flat list (no `Default agents:` / `Custom agents:` sub-headers) with a per-agent `(Tools: …)` suffix derived from each agent's `builtinToolNames` (or `*` when the agent has the full built-in set). Restored upstream's load-bearing guardrails that were missing or compressed in the old prompt: "result is not visible to the user → summarize", "trust but verify", "fresh agent / self-contained prompt" on resume, "tell the agent whether to write code or do research", "use proactively when the description says so", "MUST send a single message for parallel", and the worktree auto-cleanup behavior detail. The three redundant "Use Explore / Plan / general-purpose for …" shorthand bullets were dropped — the agent descriptions themselves now carry the canonical (and richer) selection guidance. Upstream's two `<example>` blocks at the end of "Writing the prompt" are also intentionally omitted: the per-orchestrator-turn token cost is recurring, the abstract guidance + the now-rich agent descriptions cover the same pedagogical ground, and the examples embed Anthropic-specific `<thinking>` framing that doesn't generalize across pi-ai's provider surface (OpenAI, Bedrock, Gemini, Mistral, …). All pi-specific bullets (`resume`, `steer_subagent`, `model`, `thinking`, `inherit_context`, `isolation: "worktree"`, `${scheduleGuideline}`) preserved.
23
+ - **Default agent descriptions (`general-purpose`, `Explore`, `Plan`) replaced with upstream Claude Code's verbatim wording.** Previously one-line labels (e.g. `"Fast codebase exploration agent (read-only)"`); now multi-sentence descriptions that include positive ("Use it to …") and negative ("Do NOT use it for …") guidance plus, for Explore, search-breadth hints (`"quick"` / `"medium"` / `"very thorough"`). The LLM-facing selection signal is now substantially stronger.
24
+ - **`/agents → Eject` now emits YAML-safe `description:` frontmatter.** The new Explore description contains a `: ` colon-space pattern (the search-breadth hint) and embedded quote characters — emitting it raw would have produced malformed frontmatter that the `yaml` parser would mis-parse. `ejectAgent` now wraps the description with `JSON.stringify` (a valid YAML 1.2 double-quoted scalar), so any description string round-trips cleanly through eject → re-load. Latent bug: previously unreachable because old descriptions were YAML-plain-safe.
25
+ - **`/agents → Settings` UI rewritten to inline-editable `SettingsList`.** Replaces the previous modal `ctx.ui.select` chain. All settings visible at once; `↑`/`↓` to navigate, `Space` to cycle preset values on numerics (`Max concurrency`, `Default max turns`, `Grace turns`), `Enter` to type a custom value, `Esc` to exit. Functionally equivalent — same fields, same valid ranges, same persistence behavior — but the interaction model is different. Users scripting against the old screen flow may notice.
26
+ - **`.gitignore` additions.** Added `.pi/subagents.json` (project-local subagents settings — written by `/agents → Settings`, shouldn't be committed) plus pi-runtime working files (`progress.md`, `AGENTS.md`, `CLAUDE.md`). **Migration:** if you previously committed `.pi/subagents.json` to your repo, run `git rm --cached .pi/subagents.json` to untrack — gitignore only blocks new additions.
27
+
28
+ ## [0.8.0] - 2026-05-26
29
+
30
+ > **⚠️ Breaking: peer dependencies moved from `@mariozechner/pi-*` to `@earendil-works/pi-*`.** The upstream Pi runtime relocated npm scopes on 2026-05-07; the `@mariozechner/pi-*` packages are deprecated. This release pins `@earendil-works/pi-{ai,coding-agent,tui}` at `>=0.74.0`. Hosts on the old scope must update their pi installation first (`pi update --self` handles the rename automatically) before installing this version.
31
+ >
32
+ > **Note on Node:** this release is tested against `@earendil-works/pi-coding-agent@latest` (currently `0.75.x`), which requires Node `>=22.19.0` because its bundled `undici` calls Node 22+ APIs. CI runs on Node 22. The peer range (`>=0.74.0`) technically also matches the upstream `legacy-node20` line (`0.74.x`, Node 20 compatible) and this extension contains no Node 22+ API calls of its own, but the legacy line is not exercised in CI — consumers pinning it do so at their own risk.
33
+
34
+ ### Changed
35
+ - **Peer deps migrated from `@mariozechner/pi-*` to `@earendil-works/pi-*`** ([#76](https://github.com/tintinweb/pi-subagents/issues/76) — thanks [@SEHANTA](https://github.com/SEHANTA) for the report). On **2026-05-07** the upstream Pi runtime moved npm scopes — `@mariozechner/pi-coding-agent@0.73.1` was the final publish (now deprecated on npm), and `@earendil-works/pi-coding-agent@0.74.0` shipped 30 minutes later from the same monorepo (same author, same code). `peerDependencies` now target `@earendil-works/pi-{ai,coding-agent,tui}` at `>=0.74.0`, and all `src/**` and `test/**` imports are renamed to the new scope — pure rename, no API changes. Consumers pinning the new scope no longer hit the peer-dep conflict warnings reported in [#76](https://github.com/tintinweb/pi-subagents/issues/76).
36
+ - **`ThinkingLevel` now imported from `@earendil-works/pi-ai` instead of `…/pi-agent-core`.** `src/types.ts` previously reached past the public API into `pi-agent-core` (an internal package), which only resolved because npm flat-hoisted it as a transitive of `pi-coding-agent` — under pnpm or strict-resolver setups the import failed (`TS2307: Cannot find module '@mariozechner/pi-agent-core'`). `pi-ai` re-exports `ThinkingLevel` from its public surface (`export * from "./types.ts"`), so the import goes through the documented entry point and no extra peer dep is needed.
37
+
38
+ ### Fixed
39
+ - **`.pi/subagent-schedules/` is no longer created in every working directory.** `ScheduleStore`'s constructor previously ran `mkdirSync` unconditionally, so any session with scheduling enabled left an empty `.pi/subagent-schedules/` dir behind even when nothing was ever scheduled. Directory creation is now lazy — deferred to a new private `ensureDir()` invoked at the top of `withLock`, so the dir (and its `<sessionId>.json`) appear only when a job is actually persisted. Additionally, `update`/`remove` now short-circuit on an unknown id (in-memory `jobs.has(id)` check) before taking the lock, so no-op mutations never touch disk. Read-only use (`list`/`get`/`hasName`) and constructing the store never create the dir. Pre-existing leftover dirs are not cleaned up — remove them manually.
40
+
10
41
  ## [0.7.3] - 2026-05-14
11
42
 
12
43
  ### Added
package/README.md CHANGED
@@ -31,6 +31,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
31
31
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
32
32
  - **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
33
33
  - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
34
+ - **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
34
35
 
35
36
  ## Install
36
37
 
@@ -310,9 +311,29 @@ When background agents complete, they notify the main agent. The **join mode** c
310
311
  **Configuration:**
311
312
  - Configure join mode in `/agents` → Settings → Join mode
312
313
 
314
+ ## Model Scope
315
+
316
+ **Opt-in:** off by default. Enable via `/agents → Settings → Scope models`.
317
+
318
+ When on, each subagent spawn's effective model is validated against pi's own `enabledModels` list (configured via pi's `/scoped-models` UI). pi-subagents reads that list; it doesn't manage it. Both of pi's settings files are honored: global `~/.pi/agent/settings.json` and project-local `<cwd>/.pi/settings.json`. **Project overrides global** — mirrors pi's `SettingsManager` deep-merge, so a tighter per-project scope (hand-edited into the project settings) is respected.
319
+
320
+ **Out-of-scope handling depends on source:**
321
+
322
+ | Model source | Out-of-scope behavior |
323
+ |---|---|
324
+ | Caller-supplied via `Agent({ model: "..." })` | Hard error returned to the orchestrator, listing allowed models |
325
+ | Pinned in agent frontmatter | Warning toast + the pinned model runs (frontmatter is authoritative) |
326
+ | Parent-inherited (neither set) | Warning toast + parent's model runs |
327
+
328
+ **Design:** `scopeModels` is a guardrail against the orchestrator picking unexpected models at runtime, not a hard policy against user-level config. The "frontmatter is authoritative" guarantee from v0.5.1 still holds for `model:` — caller params can't override frontmatter, and frontmatter pins run even when out of scope (with a visible warning).
329
+
330
+ **Pattern format:** only exact `provider/modelId` entries are honored (e.g. `anthropic/claude-haiku-4-5-20251001`). Glob patterns (`*sonnet*`), bare model IDs, and `:thinking` suffixes — which pi itself supports — are silently dropped here. pi's `/scoped-models` picker writes exact entries, so the limitation is invisible if you configure scope through the UI. Hand-edited globs produce an empty allowed set (scope check becomes a no-op).
331
+
332
+ **No-op safety:** if `enabledModels` is missing or empty in pi's settings, scope check skips entirely — no false positives, no spurious errors.
333
+
313
334
  ## Persistent Settings
314
335
 
315
- Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode) persist across pi restarts. Two files, merged on load:
336
+ Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, default join mode, scheduling on/off, scope models on/off) persist across pi restarts. Two files, merged on load:
316
337
 
317
338
  - **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
318
339
  - **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
@@ -5,8 +5,8 @@
5
5
  * Excess agents are queued and auto-started as running agents complete.
6
6
  * Foreground agents bypass the queue (they block the parent anyway).
7
7
  */
8
- import type { Model } from "@mariozechner/pi-ai";
9
- import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { Model } from "@earendil-works/pi-ai";
9
+ import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { type ToolActivity } from "./agent-runner.js";
11
11
  import type { AgentInvocation, AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
12
12
  export type OnAgentComplete = (record: AgentRecord) => void;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
3
3
  */
4
- import type { Model } from "@mariozechner/pi-ai";
5
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
- import { type AgentSession, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import type { Model } from "@earendil-works/pi-ai";
5
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import type { SubagentType, ThinkingLevel } from "./types.js";
8
8
  /** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
9
9
  export declare function normalizeMaxTurns(n: number | undefined): number | undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
3
3
  */
4
- import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
4
+ import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
5
5
  import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
6
6
  import { buildParentContext, extractText } from "./context.js";
7
7
  import { DEFAULT_AGENTS } from "./default-agents.js";
package/dist/context.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * context.ts — Extract parent conversation context for subagent inheritance.
3
3
  */
4
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  /** Extract text from a message content block array. */
6
6
  export declare function extractText(content: unknown[]): string;
7
7
  /**
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { existsSync, readdirSync, readFileSync } from "node:fs";
5
5
  import { basename, join } from "node:path";
6
- import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
6
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
7
7
  import { BUILTIN_TOOL_NAMES } from "./agent-types.js";
8
8
  /**
9
9
  * Scan for custom agent .md files from multiple locations.
@@ -10,7 +10,7 @@ export const DEFAULT_AGENTS = new Map([
10
10
  {
11
11
  name: "general-purpose",
12
12
  displayName: "Agent",
13
- description: "General-purpose agent for complex, multi-step tasks",
13
+ description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
14
14
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
15
15
  // inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
16
16
  // Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
@@ -26,7 +26,7 @@ export const DEFAULT_AGENTS = new Map([
26
26
  {
27
27
  name: "Explore",
28
28
  displayName: "Explore",
29
- description: "Fast codebase exploration agent (read-only)",
29
+ description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions.",
30
30
  builtinToolNames: READ_ONLY_TOOLS,
31
31
  extensions: true,
32
32
  skills: true,
@@ -68,7 +68,7 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
68
68
  {
69
69
  name: "Plan",
70
70
  displayName: "Plan",
71
- description: "Software architect for implementation planning (read-only)",
71
+ description: "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.",
72
72
  builtinToolNames: READ_ONLY_TOOLS,
73
73
  extensions: true,
74
74
  skills: true,
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Reads `enabledModels` from pi's settings (global `<agentDir>/settings.json`
3
+ * + project-local `<cwd>/.pi/settings.json`, project wins) and resolves
4
+ * entries to concrete `provider/modelId` keys for scope validation.
5
+ *
6
+ * **Project overrides global**, mirroring pi's own `SettingsManager`
7
+ * deep-merge behavior and matching the precedence we use for our own
8
+ * `subagents.json` settings (see `src/settings.ts:loadSettings`). If
9
+ * project file has `enabledModels` set, it wholly replaces global's
10
+ * (array fields are replaced, not concatenated).
11
+ *
12
+ * **Limited subset of upstream's resolveModelScope.** We support exact
13
+ * `provider/modelId` matching only. Upstream (pi-coding-agent's
14
+ * `core/model-resolver.ts`) additionally supports glob patterns
15
+ * (`*sonnet*`, `anthropic/*`), bare model IDs without provider, and
16
+ * thinking-level suffixes (`provider/*:high`). Those forms are silently
17
+ * ignored here.
18
+ *
19
+ * In practice, pi's `/scoped-models` picker writes exact `provider/modelId`
20
+ * entries, so the limitation is invisible for users who configure scope
21
+ * through pi's UI. Hand-edited settings using globs or bare IDs will
22
+ * produce an empty allowed set (scope check becomes a no-op).
23
+ *
24
+ * Example:
25
+ * enabledModels = ["anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6"]
26
+ * → resolves to { "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6" }
27
+ */
28
+ /** Minimal registry shape — only the methods resolveEnabledModels actually calls. */
29
+ export interface ModelRegistryRef {
30
+ getAll(): unknown[];
31
+ getAvailable?(): unknown[];
32
+ }
33
+ /**
34
+ * Read enabledModels from pi's settings — project-local overrides global.
35
+ * Mirrors pi's SettingsManager deep-merge for the `enabledModels` field
36
+ * (and matches our own loadSettings precedence in src/settings.ts).
37
+ * Returns undefined when neither file has the field.
38
+ */
39
+ export declare function readEnabledModels(cwd: string): string[] | undefined;
40
+ export declare function resolveEnabledModels(patterns: string[] | undefined, registry: ModelRegistryRef, cwd?: string): Set<string> | undefined;
41
+ /**
42
+ * True when `model` is in the allowed set. Centralizes the key format
43
+ * (`provider/id` lowercase) so callers don't have to reproduce it —
44
+ * both set-building (resolveExact) and lookup go through `modelKey`.
45
+ */
46
+ export declare function isModelInScope(model: {
47
+ provider: string;
48
+ id: string;
49
+ }, allowed: Set<string>): boolean;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Reads `enabledModels` from pi's settings (global `<agentDir>/settings.json`
3
+ * + project-local `<cwd>/.pi/settings.json`, project wins) and resolves
4
+ * entries to concrete `provider/modelId` keys for scope validation.
5
+ *
6
+ * **Project overrides global**, mirroring pi's own `SettingsManager`
7
+ * deep-merge behavior and matching the precedence we use for our own
8
+ * `subagents.json` settings (see `src/settings.ts:loadSettings`). If
9
+ * project file has `enabledModels` set, it wholly replaces global's
10
+ * (array fields are replaced, not concatenated).
11
+ *
12
+ * **Limited subset of upstream's resolveModelScope.** We support exact
13
+ * `provider/modelId` matching only. Upstream (pi-coding-agent's
14
+ * `core/model-resolver.ts`) additionally supports glob patterns
15
+ * (`*sonnet*`, `anthropic/*`), bare model IDs without provider, and
16
+ * thinking-level suffixes (`provider/*:high`). Those forms are silently
17
+ * ignored here.
18
+ *
19
+ * In practice, pi's `/scoped-models` picker writes exact `provider/modelId`
20
+ * entries, so the limitation is invisible for users who configure scope
21
+ * through pi's UI. Hand-edited settings using globs or bare IDs will
22
+ * produce an empty allowed set (scope check becomes a no-op).
23
+ *
24
+ * Example:
25
+ * enabledModels = ["anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6"]
26
+ * → resolves to { "anthropic/claude-sonnet-4-6", "anthropic/claude-opus-4-6" }
27
+ */
28
+ import { existsSync, readFileSync, statSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
31
+ /** Paths to pi's settings.json files: [project, global] (project takes precedence). */
32
+ function settingsPaths(cwd) {
33
+ return [
34
+ join(cwd, ".pi", "settings.json"),
35
+ join(getAgentDir(), "settings.json"),
36
+ ];
37
+ }
38
+ /** Read `enabledModels` from a single settings.json file. Undefined when missing or absent. */
39
+ function readField(path) {
40
+ if (!existsSync(path))
41
+ return undefined;
42
+ try {
43
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
44
+ if (Array.isArray(raw?.enabledModels))
45
+ return raw.enabledModels;
46
+ }
47
+ catch {
48
+ /* corrupt file — silent */
49
+ }
50
+ return undefined;
51
+ }
52
+ /**
53
+ * Read enabledModels from pi's settings — project-local overrides global.
54
+ * Mirrors pi's SettingsManager deep-merge for the `enabledModels` field
55
+ * (and matches our own loadSettings precedence in src/settings.ts).
56
+ * Returns undefined when neither file has the field.
57
+ */
58
+ export function readEnabledModels(cwd) {
59
+ const [project, global] = settingsPaths(cwd);
60
+ return readField(project) ?? readField(global);
61
+ }
62
+ /**
63
+ * Resolve enabledModels patterns → Set<"provider/modelId"> (lowercase keys).
64
+ *
65
+ * Only exact `provider/modelId` patterns are matched (case-insensitive).
66
+ * Patterns without a slash, with glob characters, or with a `:thinking`
67
+ * suffix are silently dropped. See module-level docstring for rationale.
68
+ *
69
+ * Cache: keyed on JSON.stringify(patterns) + mtime/size of *both*
70
+ * project and global settings.json files. Re-resolves when either file
71
+ * changes or the patterns argument differs.
72
+ *
73
+ * Returns undefined when no patterns are provided or no patterns match
74
+ * (scope check becomes a no-op at the call site).
75
+ */
76
+ // Module-level cache — invalidated when either settings.json changes or patterns differ.
77
+ let cachedAllowed;
78
+ let cachedHash = "";
79
+ let cachedPatternsKey = "";
80
+ /** mtime+size hash of one file, or "missing" if absent. */
81
+ function hashOf(path) {
82
+ try {
83
+ const s = statSync(path);
84
+ return `${s.mtimeMs}-${s.size}`;
85
+ }
86
+ catch {
87
+ return "missing";
88
+ }
89
+ }
90
+ export function resolveEnabledModels(patterns, registry, cwd = process.cwd()) {
91
+ // Fast path: check cache (stat both project and global settings.json files)
92
+ const patternsKey = JSON.stringify(patterns);
93
+ const [project, global] = settingsPaths(cwd);
94
+ const fileHash = `${hashOf(project)};${hashOf(global)}`;
95
+ if (fileHash === cachedHash && patternsKey === cachedPatternsKey) {
96
+ return cachedAllowed;
97
+ }
98
+ // Cache miss — resolve
99
+ if (!patterns || patterns.length === 0) {
100
+ cachedHash = fileHash;
101
+ cachedPatternsKey = patternsKey;
102
+ cachedAllowed = undefined;
103
+ return undefined;
104
+ }
105
+ const available = (registry.getAvailable?.() ?? registry.getAll());
106
+ const allowed = new Set();
107
+ for (const pattern of patterns) {
108
+ const trimmed = pattern.trim();
109
+ if (!trimmed)
110
+ continue; // skip empty/whitespace
111
+ resolveExact(trimmed, available, allowed);
112
+ }
113
+ const result = allowed.size > 0 ? allowed : undefined;
114
+ cachedHash = fileHash;
115
+ cachedPatternsKey = patternsKey;
116
+ cachedAllowed = result;
117
+ return result;
118
+ }
119
+ /**
120
+ * True when `model` is in the allowed set. Centralizes the key format
121
+ * (`provider/id` lowercase) so callers don't have to reproduce it —
122
+ * both set-building (resolveExact) and lookup go through `modelKey`.
123
+ */
124
+ export function isModelInScope(model, allowed) {
125
+ return allowed.has(modelKey(model));
126
+ }
127
+ /** Canonical lowercase `provider/id` key for the allowed set. */
128
+ function modelKey(model) {
129
+ return `${model.provider}/${model.id}`.toLowerCase();
130
+ }
131
+ /**
132
+ * Resolve exact model pattern. Example: "google/gemma-4-31b-it".
133
+ */
134
+ function resolveExact(pattern, available, allowed) {
135
+ // "provider/modelId" — exact (colon is part of id, not split)
136
+ const slashIdx = pattern.indexOf("/");
137
+ if (slashIdx === -1)
138
+ return; // bare modelId not supported
139
+ const provider = pattern.slice(0, slashIdx).toLowerCase();
140
+ const modelId = pattern.slice(slashIdx + 1).toLowerCase();
141
+ const exact = available.find(m => m.provider.toLowerCase() === provider && m.id.toLowerCase() === modelId);
142
+ if (exact) {
143
+ allowed.add(modelKey(exact));
144
+ }
145
+ }
package/dist/env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * env.ts — Detect environment info (git, platform) for subagent system prompts.
3
3
  */
4
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import type { EnvInfo } from "./types.js";
6
6
  export declare function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo>;
package/dist/index.d.ts CHANGED
@@ -9,5 +9,5 @@
9
9
  * Commands:
10
10
  * /agents — Interactive agent management menu
11
11
  */
12
- import { type ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  export default function (pi: ExtensionAPI): void;