diff --git a/package.json b/package.json index 494a3dc..6023b82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mikefreno/pygienium", - "version": "0.1.1", + "version": "0.1.2", "description": "Code hygiene extension for pi — isolated sub-agent checks that scan a target, apply fixes, and emit a findings+changes report.", "keywords": [ "pi-package", diff --git a/port-to-omp.mjs b/port-to-omp.mjs index 357df7c..a6d9aaf 100644 --- a/port-to-omp.mjs +++ b/port-to-omp.mjs @@ -256,9 +256,9 @@ const FILE_RULES = { { from: '\t\tagent.allowedTools ?? ["read", "bash", "grep", "find"];', to: '\t\tagent.allowedTools ?? ["read", "bash", "grep", "glob"];', label: "default tools" }, { from: - "\tconst { session } = await createAgentSession({\n\t\tcwd: opts.cwd,\n\t\ttools,\n\t\tsessionManager: SessionManager.inMemory(opts.cwd),\n\t\tresourceLoader: loader,\n\t});", + "\tconst { session } = await createAgentSession({\n\t\tcwd: opts.cwd,\n\t\t// Pin the sub-agent to the model the user has selected in the invoking\n\t\t// session rather than the settings default. Omitted when the caller\n\t\t// has no live session model (print/RPC modes), which keeps the\n\t\t// settings-default fallback.\n\t\t...(opts.model ? { model: opts.model } : {}),\n\t\ttools,\n\t\tsessionManager: SessionManager.inMemory(opts.cwd),\n\t\tresourceLoader: loader,\n\t});", to: - "\tconst { session } = await createAgentSession({\n\t\tcwd: opts.cwd,\n\t\ttoolNames: tools,\n\t\t// `tools` is an allowlist, not a request list.\n\t\trestrictToolNames: true,\n\t\tsessionManager: SessionManager.inMemory(opts.cwd),\n\t\t// Replace the fully rendered default prompt with the agent body.\n\t\tsystemPrompt: agent.systemPrompt,\n\t\t// Keep the sub-agent isolated: no nested extensions/skills/prompts/etc.\n\t\tdisableExtensionDiscovery: true,\n\t\tskills: [],\n\t\tpromptTemplates: [],\n\t\trules: [],\n\t\tcontextFiles: [],\n\t\tenableMCP: false,\n\t\tenableLsp: false,\n\t\t// Private registry: the host session owns the process-global \"Main\"\n\t\t// identity, so a per-run registry keeps these in-process workers\n\t\t// disjoint from the main agent.\n\t\tagentRegistry: new AgentRegistry(),\n\t});", + "\tconst { session } = await createAgentSession({\n\t\tcwd: opts.cwd,\n\t\t// Pin the sub-agent to the model the user has selected in the invoking\n\t\t// session rather than the settings default.\n\t\t...(opts.model ? { model: opts.model } : {}),\n\t\ttoolNames: tools,\n\t\t// `tools` is an allowlist, not a request list.\n\t\trestrictToolNames: true,\n\t\tsessionManager: SessionManager.inMemory(opts.cwd),\n\t\t// Replace the fully rendered default prompt with the agent body.\n\t\tsystemPrompt: agent.systemPrompt,\n\t\t// Keep the sub-agent isolated: no nested extensions/skills/prompts/etc.\n\t\tdisableExtensionDiscovery: true,\n\t\tskills: [],\n\t\tpromptTemplates: [],\n\t\trules: [],\n\t\tcontextFiles: [],\n\t\tenableMCP: false,\n\t\tenableLsp: false,\n\t\t// Private registry: the host session owns the process-global \"Main\"\n\t\t// identity, so a per-run registry keeps these in-process workers\n\t\t// disjoint from the main agent.\n\t\tagentRegistry: new AgentRegistry(),\n\t});", label: "createAgentSession", }, ], diff --git a/src/agent-runner.ts b/src/agent-runner.ts index d3d5ca4..88734f4 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -20,6 +20,7 @@ import { dirname, isAbsolute, join } from "node:path"; import type { AgentSession, AgentSessionEvent, + ExtensionCommandContext, } from "@earendil-works/pi-coding-agent"; import { loadAgents, extensionRoot, type AgentDef } from "./agents.js"; @@ -52,6 +53,11 @@ export interface AgentTaskOptions { task: string; /** Optional tool allowlist override (else uses the agent's `allowedTools`). */ allowedTools?: string[]; + /** + * The currently selected model from the invoking session. When omitted, + * `createAgentSession` falls back to the settings default model. + */ + model?: ExtensionCommandContext["model"]; /** Optional explicit agent definition (skips `loadAgents`). */ agent?: AgentDef; /** @@ -208,6 +214,11 @@ export async function defaultAgentRunner( const { session } = await createAgentSession({ cwd: opts.cwd, + // Pin the sub-agent to the model the user has selected in the invoking + // session rather than the settings default. Omitted when the caller + // has no live session model (print/RPC modes), which keeps the + // settings-default fallback. + ...(opts.model ? { model: opts.model } : {}), tools, sessionManager: SessionManager.inMemory(opts.cwd), resourceLoader: loader, diff --git a/src/commands.ts b/src/commands.ts index 63f6ff5..379e571 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -48,7 +48,7 @@ import type { SendChatMessage } from "./phases.js"; /** Narrow context slice handlers need (a subset of `ExtensionCommandContext`). */ export type PygieniumCtx = Pick< ExtensionCommandContext, - "cwd" | "mode" | "hasUI" | "ui" + "cwd" | "mode" | "hasUI" | "ui" | "model" > & { /** Optional callback to post messages to the chat window. */ sendChatMessage?: SendChatMessage; @@ -176,6 +176,7 @@ export async function handleCheckCommand( existingState: existing, ui: ctx.ui, hasUI: ctx.hasUI, + model: ctx.model, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, sendPhaseLine: ctx.sendPhaseLine, @@ -215,6 +216,7 @@ export async function handleAllCommand( only: parsed.only, ui: ctx.ui, hasUI: ctx.hasUI, + model: ctx.model, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, sendPhaseLine: ctx.sendPhaseLine, @@ -303,6 +305,7 @@ export async function handleResumeCommand( scope: { cwd, target: cwd, fix: entry.fix, rest: [] }, ui: ctx.ui, hasUI: ctx.hasUI, + model: ctx.model, existingState: state, sendChatMessage: ctx.sendChatMessage, onAgentEvent: ctx.onAgentEvent, diff --git a/src/index.ts b/src/index.ts index 59cef3f..a665044 100644 --- a/src/index.ts +++ b/src/index.ts @@ -410,6 +410,7 @@ export default async function pygieniumExtension( mode: ctx.mode, hasUI: ctx.hasUI, ui: ctx.ui, + model: ctx.model, sendChatMessage, onAgentEvent, sendPhaseLine, diff --git a/src/modes/all.ts b/src/modes/all.ts index 158f2db..0ecfd6a 100644 --- a/src/modes/all.ts +++ b/src/modes/all.ts @@ -22,10 +22,13 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; -import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent"; +import type { + AgentSessionEvent, + ExtensionCommandContext, + ExtensionUIContext, +} from "@earendil-works/pi-coding-agent"; import { getAllChecks, type CheckDefinition } from "../checks/registry.js"; import { runCheck } from "./check-runner.js"; -import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; import { createPhaseStrip, type SendChatMessage } from "../phases.js"; import { createPipelineFooter, type ItemStatus } from "../footer.js"; import { runRecon } from "../recon.js"; @@ -74,6 +77,12 @@ export interface AllRunOptions { ui?: ExtensionUIContext; /** Whether dialog-capable UI is available. */ hasUI?: boolean; + /** + * The currently selected model from the invoking session; forwarded to + * each check's sub-agents so scans run on the model the user picked, not + * the settings default. + */ + model?: ExtensionCommandContext["model"]; /** Optional callback to post completion messages into the chat. */ sendChatMessage?: SendChatMessage; /** Optional callback forwarding raw sub-agent events to the chat stream. */ @@ -369,6 +378,7 @@ export async function runAllChecks( scope: { cwd, target, fix, rest: [] }, ui: opts.ui, hasUI, + model: opts.model, existingState: state, sendChatMessage: opts.sendChatMessage, onAgentEvent: opts.onAgentEvent, diff --git a/src/modes/check-runner.ts b/src/modes/check-runner.ts index 55221fd..e97bd54 100644 --- a/src/modes/check-runner.ts +++ b/src/modes/check-runner.ts @@ -17,7 +17,10 @@ import { rm } from "node:fs/promises"; import { resolve, join } from "node:path"; -import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent"; +import type { + ExtensionCommandContext, + ExtensionUIContext, +} from "@earendil-works/pi-coding-agent"; import type { CheckDefinition, CheckScope } from "../checks/registry.js"; import { runAgentTask } from "../agent-runner.js"; import { runRecon } from "../recon.js"; @@ -84,6 +87,12 @@ export interface RunCheckOptions { ui?: ExtensionUIContext; /** Whether dialog-capable UI is available. */ hasUI?: boolean; + /** + * The currently selected model from the invoking session; forwarded to + * each sub-agent so scans run on the model the user picked, not the + * settings default. + */ + model?: ExtensionCommandContext["model"]; /** Pre-existing run state to update (for `/pygienium-all` and resume). */ existingState?: RunState; /** Optional callback to post completion messages into the chat. */ @@ -278,6 +287,7 @@ async function runCheckImplInner( cwd: scope.target, agentName: check.agentName, task: scanTask, + model: opts.model, onEvent: forward(PHASE_ANALYSIS), }); findings = scanResult.text; @@ -310,6 +320,7 @@ async function runCheckImplInner( cwd: scope.target, agentName: check.fixAgentName ?? "fixer", task: fixTask, + model: opts.model, onEvent: forward(PHASE_FIX), }); changes = fixResult.text; diff --git a/tests/check-runner.test.ts b/tests/check-runner.test.ts index a38a519..79e1dd4 100644 --- a/tests/check-runner.test.ts +++ b/tests/check-runner.test.ts @@ -110,6 +110,28 @@ describe("check-runner integration", () => { ); }); + it("forwards the selected model to every sub-agent", async () => { + const check = smokeCheck(); + const selectedModel = { + provider: "test-provider", + id: "test-model", + } as unknown as PygieniumCtx["model"]; + const seen: unknown[] = []; + setAgentRunner(async (opts) => { + seen.push(opts.model); + return fakeAgentRunner(opts); + }); + await handleCheckCommand( + check, + "--fix", + { ...stubCtx(cwd), model: selectedModel } as PygieniumCtx, + ); + + // Analysis + fix phases each dispatch one sub-agent. + expect(seen.length).toBe(2); + for (const m of seen) expect(m).toBe(selectedModel); + }); + it("persists run-state.json at the expected path", async () => { const check = smokeCheck(); await handleCheckCommand(check, "", stubCtx(cwd));