Commit 659687aa3a7a
Changed files (13)
docs
dots
pi
agent
agents
extensions
docs/superpowers/plans/2026-08-27-pi-model-modes.md
@@ -1,73 +0,0 @@
-# Pi Model Modes Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Replace Pi's mixed model presets with a consistent fast/code/heavy matrix using Gemini for work, Claude for OSS, and explicit alternative families.
-
-**Architecture:** Keep the existing data-driven mode extension unchanged. Update only its `modes.json` configuration and accompanying design documentation; existing `PI_START_MODE` values remain valid.
-
-**Tech Stack:** JSON, Pi model registry, Git
-
-## Global Constraints
-
-- Each mode resolves to one explicit provider, model, and thinking level.
-- `work` defaults to Gemini through Google Vertex AI.
-- `oss` defaults to Claude through GitHub Copilot.
-- Thinking levels are `off` for fast, `low` for code, and `medium` for heavy.
-- Do not add Gemini-through-Copilot curated modes.
-- Keep `free-code` unchanged.
-
----
-
-### Task 1: Update the curated model matrix
-
-**Files:**
-- Modify: `dots/pi/agent/modes.json`
-- Test: configuration validation commands
-
-**Interfaces:**
-- Consumes: the existing `ModesFile` schema in `dots/pi/agent/extensions/prompt-editor.ts`
-- Produces: named presets consumed by `/mode` and `PI_START_MODE`
-
-- [ ] **Step 1: Capture a failing matrix assertion**
-
-Run a Python assertion against the approved matrix before editing. It must fail because `code-work` currently selects Claude Opus instead of Gemini Flash.
-
-- [ ] **Step 2: Replace the mode definitions**
-
-Set `default` equal to `code-work`; add the approved Gemini/Claude/GPT fast, code, and heavy presets; retain `free-code`; remove `wide`, `flash`, previous, and super-fast presets.
-
-- [ ] **Step 3: Validate the matrix**
-
-Run a Python script that parses the JSON, compares every expected mode exactly, confirms the removed modes are absent, and confirms no unexpected modes exist.
-
-- [ ] **Step 4: Validate model availability**
-
-Compare every new or changed provider/model tuple with `pi --list-models` and fail if any tuple is unavailable. Preserve the unchanged `free-code` tuple even if it is absent from the current catalog.
-
-### Task 2: Verify, commit, and push
-
-**Files:**
-- Include: `dots/pi/agent/modes.json`
-- Include: `docs/superpowers/specs/2026-08-27-pi-model-modes-design.md`
-- Include: `docs/superpowers/plans/2026-08-27-pi-model-modes.md`
-
-**Interfaces:**
-- Consumes: validated configuration and documentation
-- Produces: one Conventional Commit on the current branch and an explicit push refspec
-
-- [ ] **Step 1: Review the complete diff and repository status**
-
-Confirm only the three intended files are part of this change and inspect the complete diff.
-
-- [ ] **Step 2: Run final verification**
-
-Repeat JSON matrix validation and provider/model availability checks from a clean command invocation.
-
-- [ ] **Step 3: Commit**
-
-Create one signed-off Conventional Commit describing why the modes were aligned with workload tiers and access paths.
-
-- [ ] **Step 4: Verify tracking and push explicitly**
-
-Run `git status`, identify the current branch, then push with `git push origin <branch>:<branch>`.
docs/superpowers/plans/2026-09-11-pi-subagent-model-routing.md
@@ -1,480 +0,0 @@
-# Pi Subagent Model Routing Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Prefer the parent Pi session’s provider for subagents while selecting only explicitly allowed same-tier model fallbacks.
-
-**Architecture:** Extract deterministic routing from the subagent extension into a pure `model-routing.ts` module. The extension reads settings and catalog/credential state, calls the resolver with the parent provider, and only spawns Pi after a candidate is selected. Agent files request current model versions; same-tier historical versions live solely in settings fallback chains.
-
-**Tech Stack:** TypeScript extension modules, Bun’s `node:test` runner, Pi model registry, JSON settings, Markdown agent definitions.
-
----
-
-## File structure
-
-| File | Responsibility |
-| --- | --- |
-| `dots/pi/agent/extensions/subagent/model-routing.ts` | Pure ordered candidate selection, exact model masking, catalog matching, and structured rejections. |
-| `dots/pi/agent/extensions/subagent/model-routing.test.ts` | Unit tests for provider priority, masks, same-tier fallbacks, credentials, and legacy behavior. |
-| `dots/pi/agent/extensions/subagent/index.ts` | Load and validate routing settings, collect model availability, invoke the resolver, and display failure diagnostics. |
-| `dots/pi/agent/settings.json` | Default provider fallback order plus explicit current-model fallback chains and Vertex masks. |
-| `dots/pi/agent/ensure-settings.sh` | Ensure new routing keys are merged into the runtime settings and print accurate configuration guidance. |
-| `dots/pi/agent/agents/*.md` | Pin each specialized agent to its current intended model tier. |
-| `dots/pi/agent/extensions/subagent/README.md` | Document active-provider-first resolution, masks, same-tier fallbacks, diagnostics, and current provider names. |
-
-This repository forbids commits unless the user explicitly asks. Do not add commit steps or create a commit while executing this plan.
-
-### Task 1: Define and test pure routing behavior
-
-**Files:**
-- Create: `dots/pi/agent/extensions/subagent/model-routing.ts`
-- Create: `dots/pi/agent/extensions/subagent/model-routing.test.ts`
-
-- [ ] **Step 1: Write failing tests for provider order and masks**
-
-Create `model-routing.test.ts` using Bun’s Node compatibility APIs. Define a shared catalog and credential set in the test file, then assert the parent provider wins and a provider-specific mask is skipped:
-
-```ts
-import { describe, it } from "node:test";
-import assert from "node:assert/strict";
-import { resolveSubagentModel } from "./model-routing.ts";
-
-const catalog = [
- { provider: "github-copilot", modelId: "claude-opus-5" },
- { provider: "github-copilot", modelId: "claude-opus-4-8" },
- { provider: "anthropic-vertex", modelId: "claude-opus-5" },
- { provider: "anthropic-vertex", modelId: "claude-opus-4-8" },
- { provider: "google", modelId: "claude-opus-4-8" },
-];
-const ready = new Set(catalog.map(({ provider, modelId }) => `${provider}\u0000${modelId}`));
-
-function resolve(overrides = {}) {
- return resolveSubagentModel({
- parentProvider: "github-copilot",
- providerPreference: ["anthropic-vertex", "google"],
- requestedModel: "claude-opus-5",
- modelFallbacks: { "claude-opus-5": ["claude-opus-4-8"] },
- providerModelMasks: {},
- catalog,
- ready,
- ...overrides,
- });
-}
-
-describe("resolveSubagentModel", () => {
- it("prefers the parent provider for the requested model", () => {
- assert.deepEqual(resolve(), {
- selected: { provider: "github-copilot", modelId: "claude-opus-5" },
- rejected: [],
- });
- });
-
- it("skips a masked parent provider/model pair before selection", () => {
- assert.deepEqual(
- resolve({
- providerModelMasks: { "github-copilot": ["claude-opus-5"] },
- }),
- {
- selected: { provider: "github-copilot", modelId: "claude-opus-4-8" },
- rejected: [{ provider: "github-copilot", modelId: "claude-opus-5", reason: "masked" }],
- },
- );
- });
-});
-```
-
-- [ ] **Step 2: Run the test to verify it fails**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts
-```
-
-Expected: FAIL because `./model-routing.ts` does not exist.
-
-- [ ] **Step 3: Implement the minimal pure resolver**
-
-Create `model-routing.ts`. Keep it free of Pi SDK imports and I/O so tests can exercise it directly:
-
-```ts
-export type ModelEntry = { provider: string; modelId: string };
-export type RejectionReason = "masked" | "not-in-catalog" | "missing-credentials";
-export type Rejection = { provider: string; modelId: string; reason: RejectionReason };
-export type Resolution = {
- selected: { provider: string; modelId: string } | null;
- rejected: Rejection[];
-};
-
-export type ResolveSubagentModelOptions = {
- parentProvider?: string;
- providerPreference: string[];
- requestedModel: string;
- modelFallbacks: Record<string, string[]>;
- providerModelMasks: Record<string, string[]>;
- catalog: ModelEntry[];
- ready: ReadonlySet<string>;
-};
-
-const key = (provider: string, modelId: string) => `${provider}\u0000${modelId}`;
-
-function orderedUnique(values: Array<string | undefined>): string[] {
- return [...new Set(values.filter((value): value is string => Boolean(value)))];
-}
-
-function catalogModelId(catalog: ModelEntry[], provider: string, requested: string): string | undefined {
- return catalog.find((entry) => entry.provider === provider && entry.modelId === requested)?.modelId
- ?? catalog.find((entry) => entry.provider === provider && entry.modelId.startsWith(`${requested}@`))?.modelId;
-}
-
-export function resolveSubagentModel(options: ResolveSubagentModelOptions): Resolution {
- const providers = orderedUnique([options.parentProvider, ...options.providerPreference]);
- const models = orderedUnique([options.requestedModel, ...(options.modelFallbacks[options.requestedModel] ?? [])]);
- const rejected: Rejection[] = [];
-
- for (const provider of providers) {
- const masked = new Set(options.providerModelMasks[provider] ?? []);
- for (const requestedModel of models) {
- if (masked.has(requestedModel)) {
- rejected.push({ provider, modelId: requestedModel, reason: "masked" });
- continue;
- }
- const modelId = catalogModelId(options.catalog, provider, requestedModel);
- if (!modelId) {
- rejected.push({ provider, modelId: requestedModel, reason: "not-in-catalog" });
- continue;
- }
- if (!options.ready.has(key(provider, modelId))) {
- rejected.push({ provider, modelId, reason: "missing-credentials" });
- continue;
- }
- return { selected: { provider, modelId }, rejected };
- }
- }
- return { selected: null, rejected };
-}
-```
-
-- [ ] **Step 4: Run the tests to verify they pass**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts
-```
-
-Expected: PASS, with both tests green.
-
-### Task 2: Cover all approved routing constraints with tests
-
-**Files:**
-- Modify: `dots/pi/agent/extensions/subagent/model-routing.test.ts`
-
-- [ ] **Step 1: Add failing fallback, provider fallback, credential, and no-cross-tier tests**
-
-Append these cases within the existing `describe` block:
-
-```ts
-it("uses an allowed same-tier fallback on the parent provider", () => {
- const result = resolve({
- providerModelMasks: { "github-copilot": ["claude-opus-5"] },
- });
- assert.deepEqual(result.selected, { provider: "github-copilot", modelId: "claude-opus-4-8" });
-});
-
-it("moves to configured providers after parent candidates are unavailable", () => {
- const result = resolve({
- catalog: [{ provider: "anthropic-vertex", modelId: "claude-opus-4-8" }],
- ready: new Set(["anthropic-vertex\u0000claude-opus-4-8"]),
- });
- assert.deepEqual(result.selected, { provider: "anthropic-vertex", modelId: "claude-opus-4-8" });
-});
-
-it("does not invent a cross-tier fallback", () => {
- const result = resolve({
- catalog: [{ provider: "github-copilot", modelId: "claude-sonnet-5" }],
- ready: new Set(["github-copilot\u0000claude-sonnet-5"]),
- });
- assert.equal(result.selected, null);
- assert.ok(result.rejected.every((entry) => entry.modelId.startsWith("claude-opus-")));
-});
-
-it("reports catalog and credential rejection reasons", () => {
- const result = resolve({
- catalog: [{ provider: "github-copilot", modelId: "claude-opus-5" }],
- ready: new Set(),
- });
- assert.deepEqual(result.selected, null);
- assert.ok(result.rejected.some((entry) => entry.reason === "missing-credentials"));
- assert.ok(result.rejected.some((entry) => entry.reason === "not-in-catalog"));
-});
-
-it("retains static preference behavior when no parent provider or new settings exist", () => {
- const result = resolve({
- parentProvider: undefined,
- providerPreference: ["anthropic-vertex", "github-copilot"],
- modelFallbacks: {},
- providerModelMasks: {},
- });
- assert.deepEqual(result.selected, { provider: "anthropic-vertex", modelId: "claude-opus-5" });
-});
-```
-
-- [ ] **Step 2: Run the expanded suite to verify the expected failure**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts
-```
-
-Expected: the legacy-behavior test fails because the helper’s default `parentProvider` overrides `undefined`; correct the helper to use explicit complete argument objects rather than spreading defaults over an intentional `undefined` value.
-
-- [ ] **Step 3: Correct the test helper and resolver only as needed**
-
-Replace the helper with a base object that callers can explicitly override without an `undefined` value being lost:
-
-```ts
-const baseOptions = {
- parentProvider: "github-copilot",
- providerPreference: ["anthropic-vertex", "google"],
- requestedModel: "claude-opus-5",
- modelFallbacks: { "claude-opus-5": ["claude-opus-4-8"] },
- providerModelMasks: {},
- catalog,
- ready,
-};
-
-function resolve(overrides: Partial<typeof baseOptions> = {}) {
- return resolveSubagentModel({ ...baseOptions, ...overrides });
-}
-```
-
-For the legacy test, call `resolveSubagentModel` directly with `parentProvider: undefined`; do not change production logic to accommodate a test-helper limitation.
-
-- [ ] **Step 4: Run the expanded suite**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts
-```
-
-Expected: PASS for all seven behavior cases.
-
-### Task 3: Integrate routing and pre-spawn diagnostics into the extension
-
-**Files:**
-- Modify: `dots/pi/agent/extensions/subagent/index.ts:13-80,265-425,618-850`
-
-- [ ] **Step 1: Add an integration test seam before changing spawning behavior**
-
-In `model-routing.test.ts`, add a test for Pi’s dated model aliases so the existing catalog behavior remains supported:
-
-```ts
-it("selects a dated catalog model for an undated requested ID", () => {
- const result = resolveSubagentModel({
- parentProvider: "anthropic-vertex",
- providerPreference: [],
- requestedModel: "claude-haiku-4-5",
- modelFallbacks: {},
- providerModelMasks: {},
- catalog: [{ provider: "anthropic-vertex", modelId: "claude-haiku-4-5@20251001" }],
- ready: new Set(["anthropic-vertex\u0000claude-haiku-4-5@20251001"]),
- });
- assert.deepEqual(result.selected, {
- provider: "anthropic-vertex",
- modelId: "claude-haiku-4-5@20251001",
- });
-});
-```
-
-- [ ] **Step 2: Run the unit suite before integration**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts
-```
-
-Expected: PASS; the pure resolver already supports the dated alias form.
-
-- [ ] **Step 3: Replace the old resolver with an adapter around `resolveSubagentModel`**
-
-In `index.ts`:
-
-1. Add `import { resolveSubagentModel, type Rejection } from "./model-routing.ts";`.
-2. Delete `findModelAcrossProviders` entirely.
-3. Add these configuration types and helpers near `ModelCacheEntry`:
-
-```ts
-type SubagentRoutingSettings = {
- providerPreference: string[];
- modelFallbacks: Record<string, string[]>;
- providerModelMasks: Record<string, string[]>;
-};
-
-function stringArray(value: unknown): string[] {
- return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
-}
-
-function stringArrayMap(value: unknown): Record<string, string[]> {
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
- return Object.fromEntries(Object.entries(value).map(([name, models]) => [name, stringArray(models)]));
-}
-
-function loadRoutingSettings(flagValue: string | undefined): SubagentRoutingSettings {
- const defaults: SubagentRoutingSettings = {
- providerPreference: flagValue?.split(",").map((item) => item.trim()).filter(Boolean) ?? DEFAULT_PROVIDER_PREFERENCE,
- modelFallbacks: {},
- providerModelMasks: {},
- };
- try {
- const settings = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".pi", "agent", "settings.json"), "utf-8"));
- return {
- providerPreference: flagValue ? defaults.providerPreference : stringArray(settings.subagentProviderPreference),
- modelFallbacks: stringArrayMap(settings.subagentModelFallbacks),
- providerModelMasks: stringArrayMap(settings.subagentProviderModelMasks),
- };
- } catch {
- return defaults;
- }
-}
-
-function formatRoutingFailure(requestedModel: string, rejected: Rejection[]): string {
- const lines = [`No usable subagent model for ${requestedModel}.`];
- for (const entry of rejected) lines.push(`- ${entry.provider}/${entry.modelId}: ${entry.reason}`);
- return lines.join("\n");
-}
-```
-
-4. Change `runSingleAgent` to accept `parentProvider: string | undefined` and `routing: SubagentRoutingSettings` instead of `providerPreference`.
-5. Before appending `--provider`/`--model`, build a credential-ready set by checking every catalog entry with `await modelRegistry.getApiKeyAndHeaders(model)`. Call `resolveSubagentModel` with `parentProvider`, `routing`, `modelCache ?? []`, and the ready set.
-6. When no pair is selected, return a `SingleResult` with `exitCode: 1`, no messages, and `stderr: formatRoutingFailure(agent.model, result.rejected)`. Do not append bare `--model`; this is the required no-spawn behavior.
-7. When selected, append the resolved `--provider` and `--model` arguments exactly as today.
-8. At every `runSingleAgent` call site, pass `ctx.model?.provider` and one `routing` object loaded once at the beginning of `execute` with `loadRoutingSettings(pi.getFlag("--subagent-providers") as string | undefined)`.
-
-- [ ] **Step 4: Run type/transpile and routing tests**
-
-Run:
-
-```bash
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts && bun --check index.ts
-```
-
-Expected: tests PASS and Bun reports no syntax errors.
-
-### Task 4: Configure current agent tiers and supported Vertex fallback behavior
-
-**Files:**
-- Modify: `dots/pi/agent/settings.json:3-27`
-- Modify: `dots/pi/agent/ensure-settings.sh:10-61`
-- Modify: `dots/pi/agent/agents/github-triage-analyzer.md:1-6`
-- Modify: `dots/pi/agent/agents/github-triage-deep.md:1-6`
-- Modify: `dots/pi/agent/agents/oracle.md:1-6`
-- Modify: `dots/pi/agent/agents/planner.md:1-6`
-- Modify: `dots/pi/agent/agents/researcher.md:1-6`
-- Modify: `dots/pi/agent/agents/reviewer*.md:1-6`
-- Modify: `dots/pi/agent/agents/worker.md:1-5`
-
-- [ ] **Step 1: Update settings and merge defaults**
-
-Add these keys immediately after `subagentProviderPreference` in both the JSON template and the shell script’s `REQUIRED_SETTINGS` JSON:
-
-```json
-"subagentModelFallbacks": {
- "claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
- "claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5"],
- "claude-haiku-4-5": []
-},
-"subagentProviderModelMasks": {
- "anthropic-vertex": ["claude-opus-5", "claude-opus-4-8"]
-},
-```
-
-Keep `subagentProviderPreference` as the static fallback list. The runtime parent provider is prepended by the extension, not stored in JSON. Update `ensure-settings.sh` output to print the two new keys and replace the stale `google-vertex-claude` example with `anthropic-vertex`.
-
-- [ ] **Step 2: Refresh model pins in agent definitions**
-
-Make only frontmatter model changes:
-
-```text
-claude-sonnet-5: github-triage-analyzer, oracle, planner, researcher, worker
-claude-opus-5: github-triage-deep and every reviewer*.md file
-claude-haiku-4-5: scout.md (unchanged)
-```
-
-Do not alter agent prompts, tool lists, or descriptions.
-
-- [ ] **Step 3: Validate configuration and pins against the catalog**
-
-Run:
-
-```bash
-jq empty dots/pi/agent/settings.json
-bash -n dots/pi/agent/ensure-settings.sh
-pi --list-models | rg 'anthropic-vertex +(claude-opus-5|claude-opus-4-8|claude-opus-4-7|claude-opus-4-6|claude-sonnet-5|claude-sonnet-4-6|claude-sonnet-4-5|claude-haiku-4-5)'
-rg '^model:' dots/pi/agent/agents
-```
-
-Expected: JSON and shell syntax are valid; all configured requested/fallback models appear for `anthropic-vertex`; agent pins are only Sonnet 5, Opus 5, or Haiku 4.5.
-
-### Task 5: Document runtime behavior and configuration
-
-**Files:**
-- Modify: `dots/pi/agent/extensions/subagent/README.md:1-258`
-- Modify: `dots/pi/agent/extensions/subagent/index.ts:1195-1278`
-
-- [ ] **Step 1: Update the README’s model-resolution and configuration sections**
-
-Replace the current static-preference description with these guarantees:
-
-```markdown
-1. The dispatching session's active provider is tried first.
-2. `subagentProviderPreference` supplies ordered fallback providers.
-3. For every provider, the requested model is tried before only its explicitly configured fallback IDs.
-4. `subagentProviderModelMasks` skips known-unavailable exact provider/model pairs before a child process is started.
-5. No compatible candidate produces a diagnostic; the extension does not spawn Pi or retry a failed task.
-```
-
-Use `anthropic-vertex` throughout. Include the exact default `subagentModelFallbacks` and `subagentProviderModelMasks` examples from Task 4 and explain that masks are provider-specific and must be updated after Vertex entitlement changes. Replace stale 4.5/4.6 examples with the current agent policy: scout/Haiku 4.5, standard work/Sonnet 5, deep work/Opus 5.
-
-- [ ] **Step 2: Update `/subagent-config` output**
-
-Extend the command output after “Provider Preference” to show:
-
-```ts
-lines.push("Model Fallbacks:");
-for (const [model, fallbacks] of Object.entries(routing.modelFallbacks)) {
- lines.push(` ${model}: ${fallbacks.length ? fallbacks.join(" → ") : "none"}`);
-}
-lines.push("");
-lines.push("Provider/Model Masks:");
-for (const [provider, models] of Object.entries(routing.providerModelMasks)) {
- lines.push(` ${provider}: ${models.length ? models.join(", ") : "none"}`);
-}
-```
-
-Load `routing` via the same `loadRoutingSettings` helper used by the tool. Update the example resolution wording to say the current session provider is considered first, rather than claiming the first configured provider will be selected.
-
-- [ ] **Step 3: Verify documentation references and working tree quality**
-
-Run:
-
-```bash
-rg -n 'google-vertex-claude|claude-sonnet-4-5@20250929|claude-opus-4-6' dots/pi/agent/extensions/subagent/README.md dots/pi/agent/ensure-settings.sh
-cd dots/pi/agent/extensions/subagent && bun test model-routing.test.ts && bun --check index.ts
-git diff --check
-git status --short
-```
-
-Expected: stale provider/model references are absent from the README and settings script; tests and syntax checks pass; `git diff --check` is clean; no commit is created.
-
-## Final verification checklist
-
-- [ ] `bun test dots/pi/agent/extensions/subagent/model-routing.test.ts` passes from the repository root.
-- [ ] `bun --check dots/pi/agent/extensions/subagent/index.ts` reports no syntax error.
-- [ ] `jq empty dots/pi/agent/settings.json` and `bash -n dots/pi/agent/ensure-settings.sh` pass.
-- [ ] `pi --list-models` confirms every default requested/fallback model is catalog-visible.
-- [ ] The configured Vertex mask causes `claude-opus-5` to select its first catalog-visible, credential-ready Opus fallback on Vertex, while the same model remains eligible on GitHub Copilot.
-- [ ] `git diff --check` passes and the working tree has no unrequested changes.
docs/superpowers/specs/2026-08-27-pi-model-modes-design.md
@@ -1,112 +0,0 @@
-# Pi model modes design
-
-## Goal
-
-Make Pi's curated modes simple and predictable. Each mode is an explicit provider, model, and thinking-level preset. Mode names communicate the workload tier and access path without runtime model resolution or fallback logic.
-
-## Naming
-
-Modes use:
-
-```text
-<tier>-<access>[-<family>]
-```
-
-Tiers:
-
-- `fast`: inexpensive, low-latency work
-- `code`: normal implementation and debugging
-- `heavy`: architecture, difficult debugging, and broad refactors
-
-Access paths:
-
-- `work`: Vertex AI
-- `oss`: GitHub Copilot
-
-The family suffix is omitted for the preferred family on each access path:
-
-- `work` defaults to Gemini through Google Vertex AI
-- `oss` defaults to Claude through GitHub Copilot
-
-Alternative families are explicit, such as `code-work-claude` and `code-oss-gpt`.
-
-## Thinking levels
-
-Thinking effort follows the workload tier consistently:
-
-| Tier | Thinking level |
-| --- | --- |
-| `fast` | `off` |
-| `code` | `low` |
-| `heavy` | `medium` |
-
-`high` and higher levels remain available as manual overrides but are not mode defaults, limiting unnecessary token usage and latency.
-
-## Mode matrix
-
-### Work defaults: Gemini through Vertex AI
-
-| Mode | Provider | Model | Thinking |
-| --- | --- | --- | --- |
-| `fast-work` | `google-vertex` | `gemini-3.1-flash-lite` | `off` |
-| `code-work` | `google-vertex` | `gemini-3.7-flash` | `low` |
-| `heavy-work` | `google-vertex` | `gemini-3.1-pro-preview` | `medium` |
-
-### OSS defaults: Claude through GitHub Copilot
-
-| Mode | Provider | Model | Thinking |
-| --- | --- | --- | --- |
-| `fast-oss` | `github-copilot` | `claude-haiku-4.5` | `off` |
-| `code-oss` | `github-copilot` | `claude-sonnet-5` | `low` |
-| `heavy-oss` | `github-copilot` | `claude-opus-5` | `medium` |
-
-### GPT alternatives through GitHub Copilot
-
-| Mode | Provider | Model | Thinking |
-| --- | --- | --- | --- |
-| `fast-oss-gpt` | `github-copilot` | `gpt-5.6-luna` | `off` |
-| `code-oss-gpt` | `github-copilot` | `gpt-5.6-terra` | `low` |
-| `heavy-oss-gpt` | `github-copilot` | `gpt-5.6-sol` | `medium` |
-
-### Claude alternatives through Vertex AI
-
-| Mode | Provider | Model | Thinking |
-| --- | --- | --- | --- |
-| `fast-work-claude` | `anthropic-vertex` | `claude-haiku-4-5` | `off` |
-| `code-work-claude` | `anthropic-vertex` | `claude-sonnet-5` | `low` |
-| `heavy-work-claude` | `anthropic-vertex` | `claude-opus-5` | `medium` |
-
-### Other modes
-
-- `default` points to the same provider, model, and thinking level as `code-work`.
-- `free-code` remains unchanged.
-
-Gemini models exposed through GitHub Copilot remain selectable with Pi's model selector but do not receive curated modes. Vertex AI is the preferred Gemini access path.
-
-## Removed modes
-
-Remove modes whose semantics are superseded or unclear:
-
-- `wide`
-- `flash`
-- `fast-oss-previous`
-- `code-oss-previous`
-- `super-fast-oss`
-- `super-fast-oss-gpt`
-
-## Configuration impact
-
-Update project `.envrc` generation so existing access-path defaults continue to use:
-
-- work repositories: `PI_START_MODE=code-work`
-- OSS repositories: `PI_START_MODE=code-oss` or `PI_START_MODE=code-oss-gpt`, according to the existing project policy
-
-No extension logic changes are required unless tests or documentation encode the old mode names. The primary implementation is a surgical update to `dots/pi/agent/modes.json` plus affected tests and documentation.
-
-## Validation
-
-1. Parse `modes.json` as valid JSON.
-2. Confirm every new or changed provider/model tuple appears in `pi --list-models`; preserve the unchanged `free-code` preset independently of current catalog availability.
-3. Verify removed modes no longer appear in the mode selector.
-4. Verify each new mode changes the provider, model, and thinking level as specified.
-5. Verify `PI_START_MODE=code-work` and `PI_START_MODE=code-oss` select the intended defaults without persisting them globally.
docs/superpowers/specs/2026-09-11-pi-subagent-model-routing-design.md
@@ -1,116 +0,0 @@
-# Pi subagent model routing design
-
-## Goal
-
-Make Pi subagent selection prefer the active session's provider while retaining a deterministic, configurable fallback path. Operators must be able to prevent routing a model to a provider where it is known to be unavailable, such as a model absent from a Vertex AI project entitlement.
-
-## Scope
-
-This applies to the subagent extension at `dots/pi/agent/extensions/subagent/` and the user-level agent definitions in `dots/pi/agent/agents/`.
-
-It does not probe provider entitlements at runtime or retry failed task executions. A model/provider pair is selected or rejected before a child Pi process is spawned.
-
-## Configuration
-
-Retain `subagentProviderPreference` as the ordered fallback provider list. Add two settings:
-
-```json
-{
- "subagentModelFallbacks": {
- "claude-opus-5": [
- "claude-opus-4-8",
- "claude-opus-4-7",
- "claude-opus-4-6"
- ],
- "claude-sonnet-5": [
- "claude-sonnet-4-6",
- "claude-sonnet-4-5"
- ],
- "claude-haiku-4-5": []
- },
- "subagentProviderModelMasks": {
- "anthropic-vertex": ["claude-opus-5", "claude-opus-4-8"]
- }
-}
-```
-
-- `subagentModelFallbacks` maps a requested model ID to an ordered list of permitted fallback model IDs.
-- Every fallback is explicitly configured and stays within the same model tier. In particular, Opus must never silently fall back to Sonnet or Haiku; Sonnet must never silently fall back to Haiku.
-- `subagentProviderModelMasks` maps a provider to exact model IDs that must not be routed there. A mask applies only to the named provider. It does not prevent selecting that model from GitHub Copilot or another provider.
-- Settings absent from an existing configuration preserve current behavior: configured provider order, requested model only, and no masks.
-
-## Resolution algorithm
-
-For each subagent invocation:
-
-1. Start provider candidates with the active parent session provider.
-2. Append `subagentProviderPreference`, removing duplicate providers while preserving order.
-3. Build model candidates from the requested agent model followed by its explicit configured fallback list. If the agent has no model, preserve inheritance from the parent session.
-4. Evaluate the Cartesian order by provider first, model second:
- - active provider/requested model;
- - active provider/fallback models in order;
- - first fallback provider/requested model;
- - that provider/fallback models in order; and so on.
-5. Skip a pair if the exact model is masked for that provider.
-6. For remaining pairs, select the first model known in Pi's catalog for the provider and usable with the provider's configured credentials.
-7. Spawn the child Pi process only once a candidate has been selected.
-
-This allows a GitHub Copilot parent session to use its own compatible model first, while a Vertex-backed session can deliberately select a known-enabled earlier same-tier release rather than attempting a masked model.
-
-## Failure diagnostics
-
-If no pair can be selected, do not spawn a child process. Return a diagnostic that lists candidate pairs and why each was rejected:
-
-- masked by configuration;
-- absent from Pi's model catalog for that provider;
-- catalog-present but missing provider credentials.
-
-An invocation failure after a child has started is a task/provider execution error and is not retried by this feature. Entitlement changes are handled by updating the masks and fallback lists deliberately.
-
-## Agent definitions
-
-Refresh stale model pins:
-
-- Planning, general implementation, and standard analysis agents use `claude-sonnet-5`.
-- Deep analysis and review agents use `claude-opus-5`.
-- The scout remains on `claude-haiku-4-5` for inexpensive reconnaissance.
-- Older versions belong in `subagentModelFallbacks`, rather than being pinned in individual definitions.
-
-## Implementation structure
-
-Extract routing into a small pure resolver that accepts:
-
-- parent provider;
-- configured provider preference;
-- requested model;
-- fallback map;
-- provider/model mask map;
-- catalog entries; and
-- credential availability.
-
-It returns either a selected `{ provider, modelId }` pair or structured rejection information for UI diagnostics. The extension remains responsible for loading settings/catalog data and spawning the selected child process.
-
-## Documentation
-
-Update the subagent README, settings template/merge script, and `/subagent-config` output to describe:
-
-- active-provider-first routing;
-- fallback providers;
-- model-family fallback chains;
-- provider-specific masks;
-- current provider naming (`anthropic-vertex`); and
-- how to update masks when Vertex model access changes.
-
-## Validation
-
-Add focused resolver tests for:
-
-1. The active provider winning when it offers the requested unmasked model.
-2. A masked active-provider pair being skipped before spawn.
-3. Selection of a same-tier fallback on the active provider.
-4. Selection from the configured provider fallback list when no allowed candidate exists on the active provider.
-5. No cross-tier fallback from Opus to Sonnet/Haiku or Sonnet to Haiku.
-6. Distinct structured rejection reasons for mask, catalog absence, and unavailable credentials.
-7. Existing settings with no new keys retaining current static-order/requested-model behavior.
-
-Also parse the settings JSON and ensure all model IDs in updated agent definitions and default fallback settings are present in the current Pi catalog.
dots/pi/agent/agents/researcher-work.md
@@ -0,0 +1,56 @@
+---
+name: researcher-work
+description: Broad technical research through Google Gemini.
+tools: read, bash, web_search, github_search, stack_overflow_search
+model: gemini-3.8-flash
+provider: google
+---
+
+You are a technical researcher. Your job is to conduct thorough research on technical topics, frameworks, integrations, and architectural patterns.
+
+**Research Approach:**
+
+1. **Understanding**: Clarify the research question
+2. **Discovery**: Find relevant documentation, code, examples
+3. **Analysis**: Extract key insights, patterns, and integration points
+4. **Synthesis**: Provide structured findings with actionable conclusions
+
+**Tools at your disposal:**
+
+- `web_search`: Find documentation, blog posts, discussions
+- `github_search`: Search for code examples and implementations
+- `stack_overflow_search`: Find practical Q&A and solutions
+- `read`: Examine local files and documentation
+- `bash`: Run commands to explore systems
+
+**Output Format:**
+
+## Summary
+Brief 2-3 sentence overview of findings.
+
+## Key Findings
+- **Finding 1**: Description with evidence
+- **Finding 2**: Description with evidence
+- **Finding 3**: Description with evidence
+
+## Integration Points
+Specific areas where systems/concepts connect.
+
+## Code Examples
+```language
+// Relevant code snippets
+```
+
+## Recommendations
+Actionable next steps based on research.
+
+## References
+- Links to documentation
+- Code repositories
+- Relevant discussions
+
+**Research Depth (infer from task):**
+
+- **Quick**: Surface-level scan, key concepts only
+- **Standard**: Balanced research with examples
+- **Deep**: Exhaustive analysis with multiple sources
dots/pi/agent/agents/triage-work.md
@@ -0,0 +1,133 @@
+---
+name: triage-work
+description: High-volume GitHub issue and PR investigation through Google Gemini.
+tools: read, bash, web_search
+model: gemini-3.8-flash
+provider: google
+---
+
+You are a GitHub issue/PR investigator. You receive a single issue or PR and investigate it against the codebase to produce structured findings.
+
+**You are read-only.** You NEVER post comments, apply labels, close issues, or take any action on GitHub. You only analyze and report back.
+
+## Investigation Process
+
+1. **Understand** the issue — read the body, comments, and any linked references
+2. **Classify** — determine the kind (bug/feature/question/flake/cleanup), priority, and triage status
+3. **Search** the codebase for relevant code:
+ - Error messages, stack traces mentioned in the issue
+ - Function names, type names, file paths referenced
+ - Config keys, CLI flags, API endpoints discussed
+4. **Analyze** — assess whether the issue is valid, what code is involved, and what action is appropriate
+5. **Draft** a response if one would be helpful (questions, answers, bug confirmations)
+
+## Codebase Search
+
+Use these patterns to find relevant code:
+
+```bash
+# Clone if needed (shallow, fast)
+gh repo clone OWNER/REPO -- --depth=1 --single-branch 2>/dev/null || true
+
+# Search for error messages
+rg "error text from issue" --type go -l
+rg "error text from issue" --type yaml -l
+
+# Search for symbols
+rg "FunctionName|TypeName" --type go -l
+
+# Find related tests
+rg "TestRelated" --type go -l
+
+# Check recent changes in the area
+git log --oneline --since="6 months ago" -- "relevant/path/"
+```
+
+When doing **deep** investigation, also:
+- Read the relevant source files fully
+- Trace the code path that the issue describes
+- Check error handling and edge cases
+- Look at git blame for recent changes
+- Search for closed issues with similar symptoms
+
+## Classification Reference
+
+### Kind (pick one)
+- `kind/bug` — unexpected behavior, errors, crashes, regressions
+- `kind/feature` — new functionality request
+- `kind/question` — how-to, clarification, documentation gap
+- `kind/flake` — intermittent test failure, race condition
+- `kind/cleanup` — refactoring, tech debt, code quality
+- `kind/documentation` — docs improvement needed
+- `kind/design` — design proposal, architectural discussion
+
+### Priority (pick one)
+- `priority/critical-urgent` — data loss, security, total breakage
+- `priority/important-soon` — regression, common workflow broken
+- `priority/important-longterm` — valid but not blocking
+- `priority/backlog` — nice to have
+- `priority/awaiting-more-evidence` — unclear impact, needs more info
+
+### Triage Status
+- `triage/needs-information` — can't proceed without more details from reporter
+- `triage/duplicate` — similar to another issue (always reference which one)
+- `triage/support` — user needs help, not a code change
+
+## Output Format
+
+```markdown
+## Classification
+
+- **Kind:** kind/bug
+- **Priority:** priority/important-soon
+- **Triage:** (none, or triage/needs-information, etc.)
+- **Suggested labels:** [list of labels to add]
+- **Confidence:** HIGH|MEDIUM|LOW
+
+## Summary
+
+2-3 sentence summary of the issue and its impact.
+
+## Relevant Code
+
+| File | Lines | Relevance |
+|------|-------|-----------|
+| path/to/file.go | 100-150 | Contains the function that handles X |
+| path/to/other.go | 45-60 | Error handling for the reported case |
+
+## Analysis
+
+What we found:
+- {finding 1 with evidence}
+- {finding 2 with evidence}
+
+## Root Cause (if kind/bug and deep investigation)
+
+**Hypothesis:** {what's going wrong}
+**Evidence:** {specific code references}
+**Fix approach:** {how to fix, which files to change}
+
+## Suggested Action
+
+What the maintainer should do:
+- {action 1}
+- {action 2}
+
+## Draft Comment (if helpful)
+
+> {A helpful comment that could be posted on the issue.
+> For questions: answer with code references.
+> For bugs: acknowledge, confirm, or ask for more info.
+> For features: note feasibility and relevant code.}
+>
+> If no comment is warranted, say "No comment needed — label-only action."
+```
+
+## Rules
+
+- **NEVER guess.** Only report findings supported by actual code.
+- **NEVER fabricate file paths** or function names. Verify they exist.
+- **Be honest about uncertainty.** If you can't find the relevant code, say so.
+- **Respect the depth level.** Don't over-investigate on shallow, don't under-investigate on deep.
+- **Note staleness.** Flag if the issue has had no activity in 90+ days.
+- **Check for duplicates.** If you find a very similar open/closed issue, note it.
dots/pi/agent/extensions/subagent/agents.ts
@@ -14,6 +14,7 @@ export interface AgentConfig {
description: string;
tools?: string[];
model?: string;
+ provider?: string;
systemPrompt: string;
source: "user" | "project";
filePath: string;
@@ -66,6 +67,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
description: frontmatter.description,
tools: tools && tools.length > 0 ? tools : undefined,
model: frontmatter.model,
+ provider: frontmatter.provider,
systemPrompt: body,
source,
filePath,
dots/pi/agent/extensions/subagent/index.ts
@@ -355,6 +355,7 @@ async function runSingleAgent(
if (agent.model) {
const resolution = resolveSubagentModel({
parentProvider,
+ pinnedProvider: agent.provider,
providerPreference: routing.providerPreference,
requestedModel: agent.model,
modelFallbacks: routing.modelFallbacks,
dots/pi/agent/extensions/subagent/model-routing.test.ts
@@ -66,6 +66,21 @@ describe("resolveSubagentModel", () => {
]);
});
+ it("uses only a pinned provider instead of the parent provider", () => {
+ const result = resolve({ pinnedProvider: "google" });
+ assert.deepEqual(result.selected, { provider: "google", modelId: "claude-opus-4-8" });
+ });
+
+ it("does not fall back to another provider for a pinned provider", () => {
+ const result = resolve({
+ pinnedProvider: "google",
+ catalog: [{ provider: "github-copilot", modelId: "claude-opus-5" }],
+ ready: new Set(["github-copilot\u0000claude-opus-5"]),
+ });
+ assert.equal(result.selected, null);
+ assert.ok(result.rejected.every((entry) => entry.provider === "google"));
+ });
+
it("moves to configured providers after parent candidates are unavailable", () => {
const result = resolve({
catalog: [{ provider: "anthropic-vertex", modelId: "claude-opus-4-8" }],
dots/pi/agent/extensions/subagent/model-routing.ts
@@ -8,6 +8,7 @@ export type Resolution = {
export type ResolveSubagentModelOptions = {
parentProvider?: string;
+ pinnedProvider?: string;
providerPreference: string[];
requestedModel: string;
modelFallbacks: Record<string, string[]>;
@@ -30,7 +31,9 @@ function catalogModelId(catalog: ModelEntry[], provider: string, requested: stri
}
export function resolveSubagentModel(options: ResolveSubagentModelOptions): Resolution {
- const providers = orderedUnique([options.parentProvider, ...options.providerPreference]);
+ const providers = options.pinnedProvider
+ ? [options.pinnedProvider]
+ : orderedUnique([options.parentProvider, ...options.providerPreference]);
const models = orderedUnique([options.requestedModel, ...(options.modelFallbacks[options.requestedModel] ?? [])]);
const rejected: Rejection[] = [];
dots/pi/agent/extensions/subagent/README.md
@@ -75,22 +75,26 @@ Agents live in `~/.pi/agent/agents/*.md` and specify a model ID without its prov
```markdown
---
-name: planner
-description: Creates implementation plans
-tools: read, grep, find, ls
-model: claude-sonnet-5
+name: researcher-work
+description: Broad technical research through Google Gemini.
+tools: read, bash, web_search
+model: gemini-3.8-flash
+provider: google
---
You are a planning specialist.
```
+`provider:` is optional. When set, it pins the agent to that exact provider, ignoring the parent session provider and `subagentProviderPreference`. This is appropriate for a workload with a required billing or access path. A provider-pinned agent may use explicit same-family fallbacks on the pinned provider, but never falls back to another provider.
+
Current conventions are:
-| Workload | Model |
-| --- | --- |
-| Fast reconnaissance | `claude-haiku-4-5` |
-| Planning, implementation, standard analysis | `claude-sonnet-5` |
-| Deep analysis and code review | `claude-opus-5` |
+| Workload | Provider | Model |
+| --- | --- | --- |
+| Fast reconnaissance | resolved normally | `claude-haiku-4-5` |
+| Planning, implementation, standard analysis | resolved normally | `claude-sonnet-5` |
+| Deep analysis and code review | resolved normally | `claude-opus-5` |
+| High-volume work triage and broad work research | `google` | `gemini-3.8-flash` |
## Usage
dots/pi/agent/ensure-settings.sh
@@ -20,6 +20,7 @@ REQUIRED_SETTINGS='{
"subagentModelFallbacks": {
"claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
"claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5"],
+ "gemini-3.8-flash": ["gemini-3.7-flash", "gemini-3.6-flash"],
"claude-haiku-4-5": []
},
"subagentProviderModelMasks": {
@@ -57,7 +58,7 @@ if command -v jq >/dev/null 2>&1; then
echo " - treeFilterMode: no-tools"
echo " - subagentProviderPreference: anthropic-vertex, google, llama-cpp"
- echo " - subagentModelFallbacks: same-tier Claude fallbacks"
+ echo " - subagentModelFallbacks: same-family model fallbacks"
echo " - subagentProviderModelMasks: anthropic-vertex/claude-opus-5, claude-opus-4-8"
echo " - packages: @aliou/pi-processes@0.12.0, @tmustier/pi-usage-extension@0.9.4"
else
dots/pi/agent/settings.json
@@ -23,6 +23,7 @@
"subagentModelFallbacks": {
"claude-opus-5": ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"],
"claude-sonnet-5": ["claude-sonnet-4-6", "claude-sonnet-4-5"],
+ "gemini-3.8-flash": ["gemini-3.7-flash", "gemini-3.6-flash"],
"claude-haiku-4-5": []
},
"subagentProviderModelMasks": {