fix(gsd): discover project subagents in .gsd

Prefer the documented .gsd/agents location for project-local subagents while keeping a legacy fallback to .pi/agents so existing workarounds continue to function. Add a regression test covering both paths.

Closes #2864
This commit is contained in:
mastertyko 2026-03-27 21:41:45 +01:00
parent 1d5590c19a
commit 2c0f2b3893
2 changed files with 52 additions and 2 deletions

View file

@ -0,0 +1,44 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { discoverAgents } from "../../subagent/agents.ts";
function makeProjectRoot(t: test.TestContext): string {
const root = mkdtempSync(join(tmpdir(), "gsd-subagent-agents-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
function writeAgent(root: string, configDirName: ".gsd" | ".pi", name = "ping"): string {
const agentsDir = join(root, configDirName, "agents");
mkdirSync(agentsDir, { recursive: true });
writeFileSync(
join(agentsDir, `${name}.md`),
`---\nname: ${name}\ndescription: ${name} agent\n---\nSay hello\n`,
);
return agentsDir;
}
test("discoverAgents finds project agents in .gsd/agents", (t) => {
const root = makeProjectRoot(t);
const agentsDir = writeAgent(root, ".gsd");
const discovery = discoverAgents(root, "project");
assert.equal(discovery.projectAgentsDir, agentsDir);
assert.deepEqual(discovery.agents.map((agent) => agent.name), ["ping"]);
assert.equal(discovery.agents[0]?.source, "project");
});
test("discoverAgents falls back to legacy .pi/agents when needed", (t) => {
const root = makeProjectRoot(t);
const agentsDir = writeAgent(root, ".pi");
const discovery = discoverAgents(root, "project");
assert.equal(discovery.projectAgentsDir, agentsDir);
assert.deepEqual(discovery.agents.map((agent) => agent.name), ["ping"]);
});

View file

@ -6,6 +6,8 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
const PROJECT_AGENT_DIR_CANDIDATES = [".gsd", ".pi"] as const;
export type AgentScope = "user" | "project" | "both";
export interface AgentConfig {
@ -85,8 +87,12 @@ function isDirectory(p: string): boolean {
function findNearestProjectAgentsDir(cwd: string): string | null {
let currentDir = cwd;
while (true) {
const candidate = path.join(currentDir, ".pi", "agents");
if (isDirectory(candidate)) return candidate;
// Prefer the documented project-local location while preserving support
// for older workarounds that placed agents under .pi/agents.
for (const configDir of PROJECT_AGENT_DIR_CANDIDATES) {
const candidate = path.join(currentDir, configDir, "agents");
if (isDirectory(candidate)) return candidate;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) return null;