feat(#1173): wire agent converters into descriptor-driven install path (#1227)

Extends `dispatchKindEntry` in `runtime-artifact-layout.cts` to route
agents-kind entries through a converter when the descriptor carries a
non-null `converter` field. Adds `stageAgentsForRuntimeWithConverter`
to `install-profiles.cts`, expands `VALID_CONVERTER_NAMES` with the 9
agent converter names, and adds a fail-first behavioral test suite
(9 tests) proving the new wiring end-to-end.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-14 14:20:21 -04:00
committed by GitHub
parent bf634b95c3
commit 73b7f45140
5 changed files with 420 additions and 4 deletions

View File

@@ -530,6 +530,59 @@ function stageSkillsForRuntimeAsSkills(
return stageDir;
}
/**
* Stage a converted copy of the agents directory for a given runtime.
*
* Analogous to `stageCommandsForRuntimeFlat` but for agent `.md` files. Each
* source `.md` is passed through `converter` and written as a flat `${name}.md`
* file in the staging directory. Agent filenames are kept verbatim (no prefix
* added here — the prefix is already embedded in agent stems, e.g. `gsd-planner.md`).
*
* This is used by the descriptor-driven `dispatchKindEntry` when an `agents` kind
* entry carries a non-null converter (ADR-457 / #1173). When `converter` is null,
* `agentsKind` falls back to the existing raw-copy path (`stageAgentsForProfile`).
*
* For the `full` profile (`skills === '*'`), all `.md` files are staged.
* For tiered profiles, only agents whose full stem is in `resolvedProfile.agents`
* are staged (mirrors `stageAgentsForProfile` behaviour).
*
* @param srcAgentsDir source agents directory (e.g. agents/)
* @param resolvedProfile profile filter from resolveProfile()
* @param converter (content: string) → string pure per-file converter
*/
function stageAgentsForRuntimeWithConverter(
srcAgentsDir: string,
resolvedProfile: ResolvedProfile,
converter: (content: string) => string,
): string {
if (!fs.existsSync(srcAgentsDir)) return srcAgentsDir;
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-runtime-agents-'));
try {
const entries = fs.readdirSync(srcAgentsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
// For tiered profiles, gate by agent stem (full filename without extension).
if (resolvedProfile.skills !== '*') {
const stem = entry.name.slice(0, -3);
if (!(resolvedProfile.agents instanceof Set && resolvedProfile.agents.has(stem))) {
continue;
}
}
const content = fs.readFileSync(path.join(srcAgentsDir, entry.name), 'utf8');
const converted = converter(content);
fs.writeFileSync(path.join(stageDir, entry.name), converted, 'utf8');
}
} catch (err) {
try { fs.rmSync(stageDir, { recursive: true, force: true }); } catch { /* best-effort */ }
throw err;
}
STAGED_DIRS.add(stageDir);
ensureExitCleanup();
return stageDir;
}
/**
* Stage converted command files as flat `.md` files.
*
@@ -757,6 +810,7 @@ export = {
mostRestrictiveProfile,
stageSkillsForProfile,
stageAgentsForProfile,
stageAgentsForRuntimeWithConverter,
stageSkillsForRuntimeAsSkills,
stageCommandsForRuntimeFlat,
STAGED_DIRS,

View File

@@ -21,6 +21,7 @@ import installProfiles = require('./install-profiles.cjs');
const {
stageSkillsForProfile,
stageAgentsForProfile,
stageAgentsForRuntimeWithConverter,
stageSkillsForRuntimeAsSkills,
stageCommandsForRuntimeFlat,
} = installProfiles;
@@ -192,6 +193,41 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
};
}
/**
* Build a converted-agents kind descriptor for runtimes whose agent `.md` files
* need runtime-specific frontmatter/body conversion (e.g. Copilot, Cursor, Codex).
*
* Unlike `agentsKind` (which raw-copies source files), this kind applies
* `converterName` from Runtime Artifact Conversion exports to each agent file
* during staging, writing flat `${name}.md` files to the staged directory.
*
* Agent filenames are preserved verbatim (the prefix is already embedded in the
* agent stem — e.g. `gsd-planner.md`).
*
* Mirrors the `convertedCommandsKind` pattern (#785).
*
* @param destSubpath destination subpath within configDir (e.g. 'agents')
* @param prefix filename prefix (informational; not applied here)
* @param converterName name of converter function in Runtime Artifact Conversion exports
* @param configDir runtime config dir (for .gsd-source marker resolution)
*/
function convertedAgentsKind(
destSubpath: string,
prefix: string,
converterName: string,
configDir: string,
): ArtifactKind {
return {
kind: 'agents',
destSubpath,
prefix,
stage: (resolved) => {
const converter = conversionExports[converterName] as (content: string) => string;
return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter);
},
};
}
function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): ArtifactKind {
return {
kind: 'kimi-agents',
@@ -401,7 +437,10 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi
return convertedCommandsKind(destSubpath, prefix, converter, configDir);
case 'agents':
return agentsKind(destSubpath, prefix, configDir);
if (converter == null) {
return agentsKind(destSubpath, prefix, configDir);
}
return convertedAgentsKind(destSubpath, prefix, converter, configDir);
case 'skills':
if (converter == null) {