diff --git a/packages/harness/README.md b/packages/harness/README.md index f271fa0f04..c991e87c4d 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -70,6 +70,10 @@ provider registration defaults to `us` for model discovery, and `/login` prompts region to authenticate against — which then takes over routing via `modifyModels` above. The OAuth loopback callback port can be overridden with `HARNESS_OAUTH_PORT` (default `8237`). +While an interactive session is running, `/subagents` shows the bundled agent roster with each +agent's effective model, reasoning level, and purpose. Use `/subagents all` to include project-local +`.pi/agents/*.md` definitions; project settings are applied only for trusted projects. + ## Spawn the CLI as a subprocess ```ts diff --git a/packages/harness/src/extensions/subagent/extension.ts b/packages/harness/src/extensions/subagent/extension.ts index c6669c3a14..dc959c943f 100644 --- a/packages/harness/src/extensions/subagent/extension.ts +++ b/packages/harness/src/extensions/subagent/extension.ts @@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url"; import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI, + ExtensionCommandContext, ExtensionContext, ExtensionFactory, Theme, @@ -28,7 +29,9 @@ import { } from "./format"; import { runPool } from "./process/pool"; import { renderSubagentCall, renderSubagentResult } from "./render"; +import { formatSubagentRoster } from "./roster"; import { isFailedResult, runAgent, type SingleRunResult } from "./run-agent"; +import { loadSubagentSettings } from "./settings"; import { SubagentStatusEditor } from "./status-editor"; import { renderSubagentFooterLines } from "./status-footer"; import { showSubagentStatusOverlay } from "./status-overlay"; @@ -179,6 +182,17 @@ export function createSubagentExtension( skillPaths: [fileURLToPath(new URL("./skills", import.meta.url))], })); + pi.registerCommand("subagents", { + description: + "Show the configured subagent roster. Usage: /subagents [all]", + handler: async (args: string, ctx: ExtensionCommandContext) => { + const scope = args.trim().toLowerCase() === "all" ? "both" : "bundled"; + const discovery = discoverAgents(ctx.cwd, scope); + const settings = loadSubagentSettings(ctx.cwd, ctx.isProjectTrusted()); + ctx.ui.notify(formatSubagentRoster(discovery.agents, settings), "info"); + }, + }); + pi.registerTool( defineTool({ name: "subagent", diff --git a/packages/harness/src/extensions/subagent/roster.test.ts b/packages/harness/src/extensions/subagent/roster.test.ts new file mode 100644 index 0000000000..092a7cfb93 --- /dev/null +++ b/packages/harness/src/extensions/subagent/roster.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import type { AgentConfig } from "./agents"; +import { formatSubagentRoster } from "./roster"; + +const agent: AgentConfig = { + name: "code-reviewer", + description: "Review diffs for correctness and quality", + model: "gpt-5.6-sol", + systemPrompt: "", + source: "bundled", +}; + +describe("formatSubagentRoster", () => { + it("renders the effective model and reasoning settings", () => { + const roster = formatSubagentRoster([agent], { + agentOverrides: { "code-reviewer": { thinking: "low" } }, + }); + + expect(roster).toContain("Subagent"); + expect(roster).toContain("code-reviewer"); + expect(roster).toContain("gpt-5.6-sol"); + expect(roster).toContain("low"); + expect(roster).toContain("Review diffs for correctness and quality"); + }); + + it("marks project agents and truncates long purposes", () => { + const roster = formatSubagentRoster( + [ + { + ...agent, + source: "project", + description: "a".repeat(100), + }, + ], + {}, + ); + + expect(roster).toContain("code-reviewer (project)"); + expect(roster).toContain("a".repeat(71)); + expect(roster).toContain("…"); + }); +}); diff --git a/packages/harness/src/extensions/subagent/roster.ts b/packages/harness/src/extensions/subagent/roster.ts new file mode 100644 index 0000000000..7c46c9ace9 --- /dev/null +++ b/packages/harness/src/extensions/subagent/roster.ts @@ -0,0 +1,51 @@ +import type { AgentConfig } from "./agents"; +import { applyAgentOverrides, type SubagentSettings } from "./settings"; + +const COLUMNS = [ + { title: "Subagent", width: 24 }, + { title: "Model", width: 24 }, + { title: "Reasoning", width: 10 }, + { title: "Purpose", width: 72 }, +] as const; + +function truncate(value: string, width: number): string { + if (value.length <= width) return value; + return `${value.slice(0, Math.max(0, width - 1))}…`; +} + +function cell(value: string, width: number): string { + return truncate(value, width).padEnd(width, " "); +} + +function row(values: string[]): string { + return values + .map((value, index) => cell(value, COLUMNS[index].width)) + .join(" "); +} + +export function formatSubagentRoster( + agents: AgentConfig[], + settings: SubagentSettings, +): string { + if (agents.length === 0) return "No subagents available."; + + const separatorWidth = + COLUMNS.reduce((total, column) => total + column.width, 0) + + (COLUMNS.length - 1) * 2; + const lines = [ + row(COLUMNS.map((column) => column.title)), + "-".repeat(separatorWidth), + ]; + for (const agent of agents) { + const effective = applyAgentOverrides(agent, settings); + lines.push( + row([ + agent.source === "project" ? `${agent.name} (project)` : agent.name, + effective.model ?? "inherit", + effective.thinking ?? "default", + effective.description, + ]), + ); + } + return lines.join("\n"); +}