diff --git a/.changeset/clever-seals-rest.md b/.changeset/clever-seals-rest.md new file mode 100644 index 000000000..002301d9e --- /dev/null +++ b/.changeset/clever-seals-rest.md @@ -0,0 +1,7 @@ +--- +type: Changed +pr: 1438 +--- +**Thread `isGlobal` install scope through the descriptor-driven `convertedAgentsKind` / `stageAgentsForRuntimeWithConverter` plumbing** — a prerequisite for the ADR-1235 agent-conversion cutover. No runtime declares a converted `agents` kind yet; the `capability.json` wiring is deferred to a follow-up that first ships the ADR-1235 §0 byte-for-byte parity harness (so the `/gsd:surface` / `--materialize` consumer can mirror the legacy agent pipeline before the kind goes live). The legacy `bin/install.js` agent loop remains authoritative, so installed agent output is unchanged. (#1173) + + diff --git a/docs/adr/1235-descriptor-driven-agent-conversion-migration.md b/docs/adr/1235-descriptor-driven-agent-conversion-migration.md index 86984c672..319d75d20 100644 --- a/docs/adr/1235-descriptor-driven-agent-conversion-migration.md +++ b/docs/adr/1235-descriptor-driven-agent-conversion-migration.md @@ -1,6 +1,6 @@ # ADR-1235: Migrate agent conversion to the descriptor-driven install path -- **Status:** Proposed +- **Status:** Accepted - **Date:** 2026-06-14 - **Issue:** #1235 - **Builds on:** [ADR-3660](3660-runtime-artifact-layout-module.md) (runtime artifact layout), [ADR-457](457-generated-cjs-single-source.md) (the `src/*.cts` build-at-publish tree the converters live in), [ADR-1016](1016-runtime-capability-descriptor.md) (runtime capability descriptor) diff --git a/src/install-profiles.cts b/src/install-profiles.cts index ceaf81868..4d1a929ce 100644 --- a/src/install-profiles.cts +++ b/src/install-profiles.cts @@ -548,12 +548,16 @@ function stageSkillsForRuntimeAsSkills( * * @param srcAgentsDir source agents directory (e.g. agents/) * @param resolvedProfile profile filter from resolveProfile() - * @param converter (content: string) → string pure per-file converter + * @param converter (content: string, isGlobal?: boolean) → string per-file + * converter; scope-aware converters (copilot/antigravity) + * read isGlobal, single-arg converters ignore it (#1173) + * @param isGlobal install scope passed through to the converter */ function stageAgentsForRuntimeWithConverter( srcAgentsDir: string, resolvedProfile: ResolvedProfile, - converter: (content: string) => string, + converter: (content: string, isGlobal?: boolean) => string, + isGlobal = false, ): string { if (!fs.existsSync(srcAgentsDir)) return srcAgentsDir; @@ -571,7 +575,7 @@ function stageAgentsForRuntimeWithConverter( } } const content = fs.readFileSync(path.join(srcAgentsDir, entry.name), 'utf8'); - const converted = converter(content); + const converted = converter(content, isGlobal); fs.writeFileSync(path.join(stageDir, entry.name), converted, 'utf8'); } } catch (err) { diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index 3adb6c43a..cea8a6b0f 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -175,6 +175,20 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art * Agent filenames are preserved verbatim (the prefix is already embedded in the * agent stem — e.g. `gsd-planner.md`). * + * #1173 SCOPE — plumbing only (declarations deferred): this provides the + * converter dispatch + `isGlobal` scope threading for the descriptor's `agents` + * kind, but NO runtime currently declares a converted `agents` kind in its + * `capability.json`. The descriptor declarations for the 8 non-Claude runtimes + * (copilot/antigravity/cursor/windsurf/augment/trae/codebuddy/cline) are + * DEFERRED to a follow-up that first ships the ADR-1235 §0 byte-for-byte parity + * harness, because the second `layout.kinds` consumer — `applySurface` / + * `/gsd:surface` / `--materialize` (`src/surface.cts`) — does not yet mirror the + * legacy agent pipeline (Copilot's `.agent.md` filename rename, the cross-cutting + * path-prefix rewrite + attribution, stale-file cleanup, config-reading steps), + * so declaring the kind now would regress the surface path. Until then the legacy + * `bin/install.js` agent loop remains authoritative for the real install, and + * this `convertedAgentsKind` is exercised only by synthetic-descriptor seam tests. + * * Mirrors the `convertedCommandsKind` pattern (#785). * * @param destSubpath destination subpath within configDir (e.g. 'agents') @@ -187,14 +201,24 @@ function convertedAgentsKind( prefix: string, converterName: string, configDir: string, + scope: 'local' | 'global' = 'global', ): ArtifactKind { return { kind: 'agents', destSubpath, prefix, stage: (resolved) => { - const converter = conversionExports[converterName] as (content: string) => string; - return stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, converter); + // isGlobal is threaded so scope-aware agent converters (copilot, antigravity) + // choose global-home vs workspace-relative paths; converters that only take + // (content) ignore the extra positional arg. Mirrors skillsKind's scope + // threading (#1173). + const converter = conversionExports[converterName] as (content: string, isGlobal?: boolean) => string; + return stageAgentsForRuntimeWithConverter( + findAgentsSourceRoot(configDir), + resolved, + converter, + scope === 'global', + ); }, }; } @@ -411,7 +435,7 @@ function dispatchKindEntry(entry: ArtifactKindDescriptor, runtime: string, confi if (converter == null) { return agentsKind(destSubpath, prefix, configDir); } - return convertedAgentsKind(destSubpath, prefix, converter, configDir); + return convertedAgentsKind(destSubpath, prefix, converter, configDir, scope); case 'skills': if (converter == null) { diff --git a/tests/feat-1173-agent-converters-descriptor.test.cjs b/tests/feat-1173-agent-converters-descriptor.test.cjs index 047a60ecc..970b608fc 100644 --- a/tests/feat-1173-agent-converters-descriptor.test.cjs +++ b/tests/feat-1173-agent-converters-descriptor.test.cjs @@ -285,6 +285,48 @@ describe('feat-1173: dispatchKindEntry agents converter wiring', () => { const stagedContent = fs.readFileSync(path.join(stagedDir, 'gsd-planner.md'), 'utf8'); assert.strictEqual(stagedContent, CLAUDE_AGENT_SOURCE, 'converter=null must raw-copy the agent content'); }); + + test('scope threads isGlobal to a scope-aware converter (global vs local differ)', (t) => { + // The plumbing kept by #1173 (option a): convertedAgentsKind / dispatchKindEntry + // pass the install scope to the converter as isGlobal. A scope-aware converter + // (copilot) must therefore produce different output for global vs local. This + // proves the thread is live via a synthetic descriptor — no real runtime + // declares a converted agents kind yet (declarations deferred to the ADR-1235 + // §0 parity follow-up). + const fixtureRoot = makeFixtureRoot([{ name: 'gsd-planner.md', content: CLAUDE_AGENT_SOURCE }]); + t.after(() => { + cleanup(fixtureRoot); + cleanupStagedSkills(); + }); + + const agentsEntry = { + kind: 'agents', + destSubpath: 'agents', + prefix: 'gsd-', + nesting: 'flat', + recursive: false, + converter: 'convertClaudeAgentToCopilotAgent', + }; + const registry = { + runtimes: { testruntime: { runtime: { artifactLayout: { global: [agentsEntry], local: [agentsEntry] } } } }, + }; + + const profile = { name: 'full', skills: '*', agents: new Set() }; + const stageFor = (scope) => { + const layout = resolveRuntimeArtifactLayoutFromRegistry(registry, 'testruntime', fixtureRoot, scope); + const agentKind = layout.kinds.find((k) => k.kind === 'agents'); + assert.ok(agentKind, `${scope} layout must include an agents kind`); + return fs.readFileSync(path.join(agentKind.stage(profile), 'gsd-planner.md'), 'utf8'); + }; + + const globalOut = stageFor('global'); + const localOut = stageFor('local'); + assert.notStrictEqual( + globalOut, + localOut, + 'scope-aware converter output must differ by scope — proves isGlobal is threaded from the descriptor scope', + ); + }); }); // ─── real registry: claude agents kind has converter=null ────────────────────