diff --git a/extensions/subagents/src/agent-types.ts b/extensions/subagents/src/agent-types.ts index 3f873c29..71a197c4 100644 --- a/extensions/subagents/src/agent-types.ts +++ b/extensions/subagents/src/agent-types.ts @@ -284,8 +284,8 @@ export type ParseAgentTypeResult = } | { readonly agentType?: undefined; - /** A malformed higher-precedence file blocks fallback to this name. */ - readonly rejectedName: string; + /** A malformed higher-precedence file blocks fallback to these names. */ + readonly rejectedNames: readonly string[]; readonly diagnostics: AgentTypeDiagnostic[]; }; @@ -301,9 +301,9 @@ export function parseAgentType( source = stem, ): ParseAgentTypeResult { const diagnostics: AgentTypeDiagnostic[] = []; - let rejectedName = stem; + const rejectedNames = [stem]; const fail = (message: string) => ({ - rejectedName, + rejectedNames, diagnostics: [...diagnostics, { source, message }], }); if (Buffer.byteLength(content, "utf8") > AGENT_TYPE_LIMITS.fileBytes) { @@ -334,8 +334,9 @@ export function parseAgentType( ); } // Once a safe declared name is known, a malformed/misnamed file must block - // that role rather than only its filename spelling. - rejectedName = name; + // both that role and its filename spelling: either one may be the role the + // author meant to narrow, and the other would fall back to a broader one. + if (name !== stem) rejectedNames.push(name); // A renamed file with a stale `name` would otherwise shadow a different type // than the filename suggests. if (name !== stem) { @@ -487,7 +488,7 @@ function loadDirectory(directory: string) { const result = parseAgentType(content, file.slice(0, -3), filePath); diagnostics.push(...result.diagnostics); if (result.agentType) agentTypes.push(result.agentType); - else rejectedNames.add(result.rejectedName); + else for (const name of result.rejectedNames) rejectedNames.add(name); } return { agentTypes, rejectedNames, diagnostics, blockAllFallback }; diff --git a/tests/extensions/subagents/agent-types.test.ts b/tests/extensions/subagents/agent-types.test.ts index 1155d58d..929ef345 100644 --- a/tests/extensions/subagents/agent-types.test.ts +++ b/tests/extensions/subagents/agent-types.test.ts @@ -627,6 +627,42 @@ Do not write. }); }); +test("FAIL: a misspelled name in a role file still blocks the role its filename names", async () => { + for (const layer of ["global", "project"] as const) { + await withTempDir(async (root) => { + const { agentDir, cwd } = await seed(root, { + [layer]: { + "implementer.md": `--- +name: implementor +description: Intended read-only override. +tools: [read, grep] +--- +Do not write. +`, + }, + }); + const loaded = loadAgentTypes({ agentDir, cwd, projectTrusted: true }); + assert.equal( + loaded.agentTypes.has("implementer"), + false, + `${layer} implementer.md must not expose the write-capable builtin`, + ); + assert.equal(loaded.agentTypes.has("implementor"), false); + const messages = loaded.diagnostics.map((entry) => entry.message); + assert.ok( + messages.some((message) => + /does not match filename "implementer\.md"/.test(message), + ), + ); + assert.ok( + messages.some((message) => + /blocks fallback to built-in:implementer/.test(message), + ), + ); + }); + } +}); + test("every key the parser reads is accepted without a warning", () => { const parsed = parseAgentType(VALID, "explore", "explore.md"); assert.ok(parsed.agentType);