fix(#4211): materialize Kimi's agent tree recursively during surface apply (#4371)

* fix(#4211): materialize Kimi's agent tree recursively during surface apply

kimiAgentsKind stages `gsd.yaml` + `gsd.md` + `subagents/gsd-*.{yaml,md}`, and
install copies that tree recursively (_copyStaged). Surface apply fell through
to _syncGsdDir's flat command/agent branch, which reads only top-level `*.md`:
the YAML half and the whole subagents/ subtree were ignored, and `gsd.md` was
written as `gsdgsd.md` because the flat branch re-applies kind.prefix to a name
that already carries it. A surface change could therefore corrupt Kimi's
installed artifacts while still reporting success.

Three divergences from the install path, all in src/surface.cts:

- _syncGsdDir gains a kimi-agents branch: recursive copy, then a prune scoped
  to exactly what install's _removeGsdEntries owns for this kind (the two root
  files, and gsd-*.{yaml,md} under subagents/). Everything else is user-owned
  and preserved.
- applySurface stages kimi-agents WITH agentCtx and the `skills: '*'` rule for
  an unmodified full profile, as it already does for the agents kind and as
  createRuntimeArtifactInstallPlan does for every kind — without it Kimi's
  generated subagents lost their path-prefix rewrites and attribution trailer,
  and an unmodified full profile staged only the skill-referenced subset.
- applySurface runs rewriteStagedSkillBodies for kimi-agents, which the
  install plan routes through it alongside skills.

* chore: add changeset for #4211

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Michel Moreira
2026-09-06 16:11:47 -03:00
committed by GitHub
parent acb3cc974b
commit 54085516c1
3 changed files with 144 additions and 2 deletions

View File

@@ -435,13 +435,24 @@ function applySurface(runtimeConfigDir: string, layout: Layout, manifest: Map<st
try {
for (const kind of layout.kinds) {
let staged: string;
if (kind.kind === 'agents') {
// #4211: kimi-agents is an AGENT kind — kimiAgentsKind.stage() forwards
// agentCtx into stageAgentsForRuntimeWithConverter exactly as agentsKind
// does, and createRuntimeArtifactInstallPlan hands every kind the
// context. Staging it bare here dropped the path-prefix rewrites and the
// attribution trailer from Kimi's generated subagents, and (under an
// unmodified `full` profile) staged only the skill-referenced subset the
// install path stages with `skills: '*'`.
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') {
const agentProfile = _isUnmodifiedFull ? { ...resolved, skills: '*' as const } : resolved;
staged = kind.stage(agentProfile, agentCtx);
} else {
staged = kind.stage(resolved);
}
if (kind.kind === 'skills') {
// #4211: kimi-agents takes the skill-body rewrite too —
// createRuntimeArtifactInstallPlan routes `skills` and `kimi-agents`
// through rewriteStagedSkillBodies together, so omitting it here left
// Kimi's surface-materialized prompts with unrewritten paths.
if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
runtime: layout.runtime,
configDir: layout.configDir,
@@ -638,6 +649,45 @@ function _syncGsdDir(stagedDir: string, destDir: string, kind: ArtifactKind | st
const _agentExt = runtime ? runtimeArtifactConversion.agentFileExtensionFor(runtime) : undefined;
const isRenamedAgents = !!_agentExt && kindName === 'agents';
if (kindName === 'kimi-agents') {
// #4211: Kimi's managed tree is `gsd.yaml` + `gsd.md` + `subagents/gsd-*.{yaml,md}`
// (runtime-artifact-layout.cts kimiAgentsKind), and install copies it
// RECURSIVELY (_copyStaged in src/install-engine.cts). Surface apply fell
// through to the flat command/agent branch below, which reads only `*.md`
// at the top level: the YAML half and the whole subagents/ subtree were
// dropped, and `gsd.md` was written as `gsdgsd.md` (the flat branch
// re-applies kind.prefix to a name that already carries it). A surface
// change could therefore corrupt Kimi's installed artifacts while still
// reporting success.
fs.cpSync(stagedDir, destDir, { recursive: true });
// Prune GSD-owned files the new surface no longer stages, with exactly the
// ownership rule install's _removeGsdEntries applies to this kind: the two
// root files, and `gsd-`-prefixed .yaml/.md under subagents/. Everything
// else in the directory is user-owned and is preserved.
const _rootStaged = new Set(fs.readdirSync(stagedDir));
for (const fileName of ['gsd.yaml', 'gsd.md']) {
if (!_rootStaged.has(fileName)) {
try { fs.rmSync(path.join(destDir, fileName), { force: true }); } catch { /* ignore */ }
}
}
const _stagedSubagentsDir = path.join(stagedDir, 'subagents');
const _destSubagentsDir = path.join(destDir, 'subagents');
const _stagedSubagents = fs.existsSync(_stagedSubagentsDir)
? new Set(fs.readdirSync(_stagedSubagentsDir))
: new Set<string>();
if (fs.existsSync(_destSubagentsDir)) {
for (const entry of fs.readdirSync(_destSubagentsDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
if (!entry.name.startsWith('gsd-')) continue;
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md')) continue;
if (_stagedSubagents.has(entry.name)) continue;
try { fs.rmSync(path.join(_destSubagentsDir, entry.name), { force: true }); } catch { /* ignore */ }
}
}
return;
}
if (kindName === 'skills') {
// Skills kind: work with directories, not files.
// Each staged entry is a directory named ${prefix}${stem}.