import assert from "node:assert/strict"; import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import * as path from "node:path"; import test from "node:test"; import type { AgentProfile, WorkflowFacts } from "./domain.ts"; import { eligibleProfiles, loadAgentProfiles, selectInitialDiscoveryProfile, } from "./profiles.ts"; async function withTempDir(run: (directory: string) => Promise) { const directory = await mkdtemp(path.join(tmpdir(), "pi-workflow-profiles-")); try { await run(directory); } finally { await rm(directory, { recursive: true, force: true }); } } function profileDocument(options: { name: string; description?: string; roles?: string; selection?: string; extra?: string; body?: string; }) { return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\nroles: ${options.roles ?? "[discover]"}\n${options.extra ?? ""}selection:\n${options.selection ?? " stages: [discover]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n"}---\n\n${options.body ?? "Follow the assignment."}\n`; } function standardAgentDocument(options: { name: string; description?: string; model?: string; tools?: string; thinking?: string; body?: string; }) { return `---\nname: ${options.name}\ndescription: ${options.description ?? options.name}\n${options.model ? `model: ${options.model}\n` : ""}${options.tools ? `tools: ${options.tools}\n` : ""}${options.thinking ? `thinking: ${options.thinking}\n` : ""}subagent_agents: scout, researcher\nsystem-prompt: append\nauto-exit: true\n---\n\n${options.body ?? "Follow the standard assignment."}\n`; } async function writeProfile( root: string, file: string, options: Parameters[0], ) { await mkdir(root, { recursive: true }); await writeFile(path.join(root, file), profileDocument(options)); } async function writeStandardAgent( root: string, file: string, options: Parameters[0], ) { await mkdir(root, { recursive: true }); await writeFile(path.join(root, file), standardAgentDocument(options)); } test("bundled profiles form a complete deterministic catalog", async () => { await withTempDir(async (directory) => { const catalog = await loadAgentProfiles({ cwd: directory, agentDir: path.join(directory, "agent"), projectTrusted: false, }); assert.deepEqual( catalog.profiles.map((profile) => profile.name), [ "feature-designer", "feature-implementer", "feature-verifier", "general-discovery", "independent-reviewer", "ui-prototyper", ], ); assert.equal(catalog.diagnostics.length, 0); assert.ok( catalog.profiles.every((profile) => /^[a-f0-9]{64}$/.test(profile.digest)), ); }); }); test("standard package, global, and trusted project agents use role overlays and precedence", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const packageRoot = path.join(directory, "package-agents"); const agentDir = path.join(directory, "agent"); const globalRoot = path.join(agentDir, "agents"); const projectRoot = path.join(directory, ".pi", "agents"); await writeProfile(bundled, "general.md", { name: "general-discovery", }); await writeStandardAgent(packageRoot, "scout.md", { name: "scout", model: "openai-codex/gpt-test", tools: "read, grep, find, ls", thinking: "high", body: "Package scout.", }); await writeStandardAgent(packageRoot, "worker.md", { name: "worker", tools: "read, write, edit, bash", body: "Package worker.", }); await writeStandardAgent(globalRoot, "scout.md", { name: "scout", body: "Global scout.", }); await writeStandardAgent(globalRoot, "unmapped.md", { name: "unmapped", }); await writeStandardAgent(projectRoot, "worker.md", { name: "worker", body: "Project worker.", }); const catalog = await loadAgentProfiles({ cwd: directory, agentDir, bundledRoot: bundled, standardPackageRoot: packageRoot, projectRoot, projectTrusted: true, standardAgents: { enabled: true, roles: { scout: ["discover"], worker: ["implement"] }, }, }); const scout = catalog.profiles.find((profile) => profile.name === "scout"); assert.equal(scout?.source, "standard-global"); assert.equal(scout?.instructions, "Global scout."); assert.deepEqual(scout?.roles, ["discover"]); assert.deepEqual(scout?.selection.stages, ["discover"]); const worker = catalog.profiles.find((profile) => profile.name === "worker"); assert.equal(worker?.source, "standard-project"); assert.equal(worker?.instructions, "Project worker."); assert.deepEqual(worker?.roles, ["implement"]); assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false); }); }); test("package and global standard definitions cannot self-declare workflow roles", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const packageRoot = path.join(directory, "package-agents"); await writeProfile(bundled, "general.md", { name: "general-discovery" }); await writeProfile(packageRoot, "worker.md", { name: "worker", roles: "[review]", selection: " stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n", body: "Attempted self-declared reviewer.", }); await writeProfile(packageRoot, "unmapped.md", { name: "unmapped", roles: "[review]", selection: " stages: [review]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n", }); const catalog = await loadAgentProfiles({ cwd: directory, agentDir: path.join(directory, "agent"), bundledRoot: bundled, standardPackageRoot: packageRoot, projectTrusted: false, standardAgents: { enabled: true, roles: { worker: ["implement"] }, }, }); const worker = catalog.profiles.find((profile) => profile.name === "worker"); assert.equal(worker?.source, "standard-package"); assert.deepEqual(worker?.roles, ["implement"]); assert.deepEqual(worker?.selection.stages, ["implement"]); assert.equal(catalog.profiles.some((profile) => profile.name === "unmapped"), false); }); }); test("a trusted workflow profile keeps its declared roles when its name also has a standard overlay", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const packageRoot = path.join(directory, "package-agents"); const projectRoot = path.join(directory, ".pi", "agents"); await writeProfile(bundled, "general.md", { name: "general-discovery" }); await writeStandardAgent(packageRoot, "scout.md", { name: "scout" }); await writeProfile(projectRoot, "scout.md", { name: "scout", roles: "[verify]", selection: " stages: [verify]\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n", body: "Project verifier.", }); const catalog = await loadAgentProfiles({ cwd: directory, agentDir: path.join(directory, "agent"), bundledRoot: bundled, standardPackageRoot: packageRoot, projectRoot, projectTrusted: true, standardAgents: { enabled: true, roles: { scout: ["discover"] }, }, }); const scout = catalog.profiles.find((profile) => profile.name === "scout"); assert.equal(scout?.source, "project"); assert.deepEqual(scout?.roles, ["verify"]); assert.deepEqual(scout?.selection.stages, ["verify"]); assert.equal(scout?.instructions, "Project verifier."); }); }); test("standard agents are disabled explicitly and ignored for untrusted projects", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const packageRoot = path.join(directory, "package-agents"); const projectRoot = path.join(directory, ".pi", "agents"); await writeProfile(bundled, "general.md", { name: "general-discovery" }); await writeStandardAgent(packageRoot, "scout.md", { name: "scout" }); await writeStandardAgent(projectRoot, "worker.md", { name: "worker" }); const disabled = await loadAgentProfiles({ cwd: directory, agentDir: path.join(directory, "agent"), bundledRoot: bundled, standardPackageRoot: packageRoot, projectRoot, projectTrusted: true, standardAgents: { enabled: false, roles: { scout: ["discover"], worker: ["implement"] }, }, }); assert.deepEqual(disabled.profiles.map((profile) => profile.name), ["general-discovery"]); }); }); test("trusted project profiles override bundled profiles while first duplicate wins", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const project = path.join(directory, "project"); await writeProfile(bundled, "10-shared.md", { name: "shared", description: "bundled first", }); await writeProfile(bundled, "20-shared.md", { name: "shared", description: "bundled duplicate", }); await writeProfile(project, "10-shared.md", { name: "shared", description: "project override", }); await writeFile(path.join(project, "20-broken.md"), "not frontmatter"); const untrusted = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectRoot: project, projectTrusted: false, }); assert.equal(untrusted.profiles[0]?.description, "bundled first"); assert.equal(untrusted.diagnostics.length, 1); assert.doesNotMatch(untrusted.diagnostics[0]?.sourcePath ?? "", /project/); const trusted = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectRoot: project, projectTrusted: true, }); assert.equal(trusted.profiles[0]?.description, "project override"); assert.equal(trusted.profiles[0]?.source, "project"); assert.ok( trusted.diagnostics.some((item) => /duplicate profile/.test(item.message), ), ); assert.ok( trusted.diagnostics.some((item) => /missing YAML frontmatter/.test(item.message), ), ); }); }); test("malformed profiles are isolated and unknown fields produce warnings", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); await writeProfile(bundled, "good.md", { name: "good-profile", description: "Coordinates https://example.test: path", roles: "\n - discover", extra: "unknownField: ignored\nmetadata:\n owner: team\nlabels:\n - internal\n", selection: " stages:\n - discover\n anyTags: []\n allTags: []\n pathGlobs: []\n keywords: []\n unknownSelection:\n owner: team\n", }); await writeFile( path.join(bundled, "quoted.md"), String.raw`--- name: quoted-profile description: "Path C:\\" # comment roles: [discover] selection: stages: [discover] anyTags: [] allTags: [] pathGlobs: [] keywords: ["path\\", dashboard] # comment --- Quoted values remain valid. `, ); await writeFile( path.join(bundled, "single.md"), String.raw`--- name: single-profile description: 'Path C:\' # comment roles: [discover] selection: stages: [discover] anyTags: [] allTags: [] pathGlobs: [] keywords: ['path\', dashboard] # comment --- Single-quoted values keep literal backslashes. `, ); await writeProfile(bundled, "bad-provider.md", { name: "bad-provider", extra: "model: provider/model\nprovider: provider\n", }); await writeProfile(bundled, "bad-name.md", { name: "Bad Name" }); const catalog = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectTrusted: false, }); assert.deepEqual( catalog.profiles.map((profile) => profile.name), ["good-profile", "quoted-profile", "single-profile"], ); assert.equal( catalog.profiles[0]?.description, "Coordinates https://example.test: path", ); assert.deepEqual(catalog.profiles[1]?.selection.keywords, [ "path\\", "dashboard", ]); assert.deepEqual(catalog.profiles[2]?.selection.keywords, [ "path\\", "dashboard", ]); assert.ok( catalog.diagnostics.some( (item) => item.severity === "warning" && /unknown profile/.test(item.message), ), ); assert.ok( catalog.diagnostics.some((item) => /unknown selection/.test(item.message), ), ); assert.ok( catalog.diagnostics.some((item) => /provider requires/.test(item.message), ), ); assert.ok( catalog.diagnostics.some((item) => /lowercase kebab/.test(item.message)), ); }); }); test("a symlinked profile root is rejected", async () => { await withTempDir(async (directory) => { const actual = path.join(directory, "actual"); const linked = path.join(directory, "linked"); await writeProfile(actual, "agent.md", { name: "external-root" }); await symlink(actual, linked); await assert.rejects( loadAgentProfiles({ cwd: directory, bundledRoot: linked, projectTrusted: false, }), /profile root must not be a symlink/, ); }); }); test("a symlinked project profile parent is rejected", async () => { await withTempDir(async (directory) => { const repository = path.join(directory, "repository"); const bundled = path.join(directory, "bundled"); const externalPi = path.join(directory, "external-pi"); await mkdir(repository, { recursive: true }); await writeProfile(bundled, "agent.md", { name: "bundled-agent" }); await writeProfile(path.join(externalPi, "agents"), "agent.md", { name: "external-project", }); await symlink(externalPi, path.join(repository, ".pi")); await assert.rejects( loadAgentProfiles({ cwd: repository, bundledRoot: bundled, projectTrusted: true, }), /project profile ancestry must not be a symlink/, ); }); }); test("profile files and symlinks must resolve inside their source root", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const outside = path.join(directory, "outside.md"); await mkdir(bundled, { recursive: true }); await writeFile( path.join(bundled, "internal.profile"), profileDocument({ name: "internal-link" }), ); await writeFile(outside, profileDocument({ name: "outside-link" })); await symlink("internal.profile", path.join(bundled, "internal.md")); await symlink(outside, path.join(bundled, "outside.md")); await symlink("missing.profile", path.join(bundled, "broken.md")); const catalog = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectTrusted: false, }); assert.deepEqual( catalog.profiles.map((profile) => profile.name), ["internal-link"], ); assert.ok(catalog.profiles[0]?.resolvedPath.endsWith("internal.profile")); assert.ok( catalog.diagnostics.some((item) => /escapes the profile root/.test(item.message), ), ); assert.ok( catalog.diagnostics.some((item) => item.sourcePath.endsWith("broken.md")), ); }); }); test("the nearest trusted ancestor project profile root is selected", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); const project = path.join(directory, ".pi", "agents"); const nestedCwd = path.join(directory, "packages", "app"); await mkdir(nestedCwd, { recursive: true }); await writeProfile(bundled, "agent.md", { name: "shared-agent", description: "bundled", }); await writeProfile(project, "agent.md", { name: "shared-agent", description: "nearest project", }); const trusted = await loadAgentProfiles({ cwd: nestedCwd, bundledRoot: bundled, projectTrusted: true, }); assert.equal(trusted.profiles[0]?.description, "nearest project"); const untrusted = await loadAgentProfiles({ cwd: nestedCwd, bundledRoot: bundled, projectTrusted: false, }); assert.equal(untrusted.profiles[0]?.description, "bundled"); }); }); test("active profile contents are pinned by digest", async () => { await withTempDir(async (directory) => { const bundled = path.join(directory, "bundled"); await writeProfile(bundled, "agent.md", { name: "pinned-agent", body: "Original instructions.", }); const first = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectTrusted: false, }); await writeProfile(bundled, "agent.md", { name: "pinned-agent", body: "Changed instructions.", }); const second = await loadAgentProfiles({ cwd: directory, bundledRoot: bundled, projectTrusted: false, }); assert.equal(first.profiles[0]?.instructions, "Original instructions."); assert.notEqual(first.profiles[0]?.digest, second.profiles[0]?.digest); }); }); function agent( name: string, selection: AgentProfile["selection"], source: AgentProfile["source"] = "bundled", ): AgentProfile { return { name, description: name, roles: ["discover"], selection, instructions: "Discover.", source, sourcePath: name, resolvedPath: name, digest: name.padEnd(64, "0"), }; } const baseFacts: Omit = { taskKind: "feature", requestTerms: ["dashboard"], candidatePaths: ["src/routes/dashboard/page.tsx"], languages: ["typescript"], tags: ["frontend", "ui"], riskTags: [], }; test("eligibility records exclusion reasons and selects the most specific discovery profile", () => { const fallback = agent("general", { stages: ["discover"], anyTags: [], allTags: [], pathGlobs: [], keywords: [], }); const dashboard = agent("dashboard", { stages: ["discover"], anyTags: ["ui"], allTags: ["frontend"], pathGlobs: ["src/**/dashboard/*.tsx"], keywords: ["dashboard"], }); const backend = agent("backend", { stages: ["discover"], anyTags: ["backend"], allTags: [], pathGlobs: [], keywords: [], }); const result = selectInitialDiscoveryProfile( [fallback, dashboard, backend], baseFacts, ); assert.equal(result.selected.name, "dashboard"); const excluded = result.evaluations.find( (item) => item.profile.name === "backend", ); assert.equal(excluded?.eligible, false); assert.deepEqual(excluded?.reasons, ["no optional tag matched"]); const preDiscovery = eligibleProfiles( [fallback, dashboard], { ...baseFacts, stage: "discover", requestTerms: [], candidatePaths: [], tags: [], }, "discover", ); assert.equal( preDiscovery.find((item) => item.eligible)?.profile.name, "general", ); }); test("an eligible standard agent replaces the bundled fallback for its role", () => { const emptySelection: AgentProfile["selection"] = { stages: ["discover"], anyTags: [], allTags: [], pathGlobs: [], keywords: [], }; const bundled = agent("general-discovery", emptySelection); const scout = agent("scout", emptySelection, "standard-package"); const result = selectInitialDiscoveryProfile( [bundled, scout], baseFacts, ); assert.equal(result.selected.name, "scout"); assert.deepEqual( result.evaluations.find((item) => item.profile.name === "general-discovery")?.reasons, ["eligible standard agent available for role discover"], ); }); test("eligibility enforces every criterion and uses a code-point name tie-breaker", () => { const emptySelection: AgentProfile["selection"] = { stages: ["discover"], anyTags: [], allTags: [], pathGlobs: [], keywords: [], }; const wrongRole = { ...agent("wrong-role", emptySelection), roles: ["design"] as AgentProfile["roles"], }; const wrongStage = agent("wrong-stage", { ...emptySelection, stages: ["design"], }); const missingAllTag = agent("missing-all-tag", { ...emptySelection, allTags: ["backend"], }); const missingAnyTag = agent("missing-any-tag", { ...emptySelection, anyTags: ["backend"], }); const missingPath = agent("missing-path", { ...emptySelection, pathGlobs: ["server/**/*.ts"], }); const missingKeyword = agent("missing-keyword", { ...emptySelection, keywords: ["api"], }); const evaluations = eligibleProfiles( [ wrongRole, wrongStage, missingAllTag, missingAnyTag, missingPath, missingKeyword, ], { ...baseFacts, stage: "discover" }, "discover", ); const reasons = new Map( evaluations.map((evaluation) => [ evaluation.profile.name, evaluation.reasons, ]), ); assert.deepEqual(reasons.get("wrong-role"), [ "does not support role discover", ]); assert.deepEqual(reasons.get("wrong-stage"), [ "does not support stage discover", ]); assert.deepEqual(reasons.get("missing-all-tag"), [ "missing required tags: backend", ]); assert.deepEqual(reasons.get("missing-any-tag"), ["no optional tag matched"]); assert.deepEqual(reasons.get("missing-path"), ["no candidate path matched"]); assert.deepEqual(reasons.get("missing-keyword"), [ "no request keyword matched", ]); const tie = eligibleProfiles( [agent("zeta", emptySelection), agent("alpha", emptySelection)], { ...baseFacts, stage: "discover" }, "discover", ); assert.deepEqual( tie .filter((evaluation) => evaluation.eligible) .map((evaluation) => evaluation.profile.name), ["alpha", "zeta"], ); });