refactor(#1099): extract runtime artifact conversion module (#1100)

* refactor(#1099): extract runtime artifact conversion module

* docs(#1099): update runtime conversion inventory

* docs(#1099): reconcile runtime conversion inventory count

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Tom Boucher
2026-06-13 10:27:12 -04:00
committed by GitHub
parent f116b76128
commit 89a8f915a4
9 changed files with 2222 additions and 14 deletions

2
.gitignore vendored
View File

@@ -130,6 +130,8 @@ build/
/gsd-core/bin/lib/worktree-base-ref.cjs
/gsd-core/bin/lib/worktree-safety.cjs
/gsd-core/bin/lib/planning-workspace.cjs
/gsd-core/bin/lib/command-roster.cjs
/gsd-core/bin/lib/runtime-artifact-conversion.cjs
/gsd-core/bin/lib/runtime-artifact-layout.cjs
/gsd-core/bin/lib/runtime-config-adapter-registry.cjs
/gsd-core/bin/lib/runtime-hooks-surface.cjs

View File

@@ -139,6 +139,12 @@ Module owning which skills and agents are written to runtime config directories
### Runtime Artifact Layout Module
Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `gsd-ns-*` routers + concrete skills under `<router>/skills/<name>/`) or the flat `skills/gsd-<stem>/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. The shared accessor `getInstallExports` (exported from `runtime-artifact-layout.cjs`) is the single-source seam through which `surface.cjs` reaches `computePathPrefix` and `applyRuntimeContentRewritesInPlace`; the resolved `scope` (`'local'`|`'global'`) is now carried on the `Layout` object returned by `resolveRuntimeArtifactLayout` so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). See ADR-3660.
### Runtime Artifact Conversion Module
Sibling Module to Runtime Artifact Layout Module. Owns projection from canonical Claude-authored command/agent/skill markdown into runtime-specific artifact bodies, including converter selection, frontmatter/body normalization, runtime path rewrites, and staged artifact generation. Runtime Artifact Layout remains responsible for filesystem placement (`kind`, destination subpath, prefix, nesting); Runtime Artifact Conversion owns the content Implementation behind that placement seam so install, uninstall/surface parity, and future plugin/package projections stop reaching back through `bin/install.js` for converter functions or `GSD_TEST_MODE`-guarded installer exports. Chosen direction: sibling Module, not an expanded Layout Module, to preserve ADR-3660's narrow placement responsibility while deepening artifact content locality. First slice: relocate only the layout-reached conversion family (`convertClaudeCommandTo*Skill`, converted command-file emitters, `buildKimiAgentArtifacts`) plus the minimal helper closure they need; do not leave helper dependencies in `bin/install.js` because that would preserve the same shallow seam under a new filename. Installer integration decision: `bin/install.js` imports the conversion Module at top level and re-exports the moved names for compatibility; the conversion Module must not import `bin/install.js` or Runtime Artifact Layout, so the dependency direction becomes installer/layout Adapters -> conversion Module, never conversion -> installer. First-slice Interface decision: export the existing compatibility names only; do not introduce a grouped `convertRuntimeArtifact` Interface until after relocation proves byte-for-byte behavior.
### Command Roster Module
Tiny read-only helper Module owning discovery of canonical `commands/gsd/*.md` command stems for artifact conversion and runtime projection. It is a sibling dependency of Runtime Artifact Conversion Module, not part of conversion itself: conversion consumes a roster to safely rewrite `gsd:` / `/gsd-` references, while roster discovery owns filesystem/catalog knowledge. First slice: extract existing `readGsdCommandNames` behavior behind this Module instead of moving it into Runtime Artifact Conversion Module or keeping it as installer-owned state.
### Runtime Install Policy Module
Projects a pure, typed install plan for a given runtime by composing artifact placements (Runtime Artifact Layout Module), command text (Shell Command Projection Module), and per-runtime config intentions — with no filesystem IO or format-specific serialization. Runtime-specific adapters consume the plan and execute concrete file mutations and config rendering. See ADR-58.

View File

@@ -26,8 +26,8 @@ const {
// fs.readdirSync + RegExp work for every skill.
const {
transformContentToHyphen,
readCmdNames: readGsdCommandNames,
} = require(path.join(__dirname, '..', 'scripts', 'fix-slash-commands.cjs'));
readGsdCommandNames,
} = require('../gsd-core/bin/lib/command-roster.cjs');
const {
resolveAntigravityGlobalDir,
getGlobalConfigDir,
@@ -38,6 +38,7 @@ const {
readBaseRefFromSettings,
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
// Canonical set of hook files shipped to users. Imported here so writeManifest()
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
// the manifest and the installed hooks/ dir structurally identical. Avoids the
@@ -12281,6 +12282,7 @@ module.exports = {
parseConfigDirFromArgs,
cleanupLegacyGsdCc,
_applyRuntimeRewrites,
...runtimeArtifactConversion,
};
// Main logic — only run when not loaded as a module for testing

View File

@@ -284,6 +284,7 @@
"code-review-flags.cjs",
"command-aliases.cjs",
"command-arg-projection.cjs",
"command-roster.cjs",
"command-routing-hub.cjs",
"commands.cjs",
"config-loader.cjs",
@@ -344,6 +345,7 @@
"roadmap-parser.cjs",
"roadmap-upgrade.cjs",
"roadmap.cjs",
"runtime-artifact-conversion.cjs",
"runtime-artifact-layout.cjs",
"runtime-config-adapter-registry.cjs",
"runtime-homes.cjs",

View File

@@ -372,7 +372,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
---
## CLI Modules (110 shipped)
## CLI Modules (112 shipped)
Full listing: `gsd-core/bin/lib/*.cjs`.
@@ -395,6 +395,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `code-review-flags.cjs` | Typed flag parser for `/gsd:code-review`; exports `parseCodeReviewFlags(argv)` (→ `{ fix, all, auto, depth, files }`) and `resolveCodeReviewWorkflow(flags)` (→ `'code-review.md' \| 'code-review-fix.md'`); canonical dispatch seam for `--fix`/`--all`/`--auto` routing |
| `command-aliases.cjs` | Alias/subcommand metadata for manifest-backed family routers |
| `command-arg-projection.cjs` | Typed flag and positional argument projection helpers shared across command-family routers |
| `command-roster.cjs` | Read-only discovery of canonical `commands/gsd/*.md` command stems for runtime artifact conversion and namespace rewrites |
| `command-routing-hub.cjs` | Pure-result dispatch hub that centralizes mode decision (SDK vs CJS), error taxonomy, and no-throw contract for all command-family routers (#3788) |
| `commands.cjs` | Misc CLI commands (slug, timestamp, todos, scaffolding, stats) |
| `config-loader.cjs` | Project config loading — defaults merge, legacy-key migration, workstream overlay, unknown-key/profile-override validation (extracted from `core.cjs`, ADR-857) |
@@ -455,6 +456,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `roadmap-parser.cjs` | ROADMAP.md parsing — milestone slicing, current-milestone extraction, phase/milestone lookups, milestone-phase filter (extracted from `core.cjs`, ADR-857) |
| `roadmap-upgrade.cjs` | Migration tool for converting legacy `Phase N` entries to milestone-prefixed `Phase M-NN` convention; `computeMigrationPlan` + `applyMigration` with dry-run default and atomic rollback |
| `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress |
| `runtime-artifact-conversion.cjs` | Runtime artifact conversion module — projects Claude-authored commands, agents, and skills into runtime-specific artifact bodies while preserving installer compatibility exports |
| `runtime-artifact-layout.cjs` | Runtime artifact layout module — resolves the artifact directory shapes (commands, agents, skills) for each supported runtime; single source of truth for per-runtime artifact placement (#3663) |
| `runtime-config-adapter-registry.cjs` | Explicit runtime config adapter registry — resolves per-runtime config-mutation install intent (install surface, shared-settings gate, finish-phase permission writer); see ADR-58. |
| `runtime-hooks-surface.cjs` | Runtime hooks surface module — standalone hook-surface writer functions extracted from bin/install.js (ADR-857 phase 5f-1); owns Cline/Cursor/Copilot/Codex hook artifact generation and reconciliation. |

View File

@@ -96,6 +96,8 @@ export default tseslint.config(
'gsd-core/bin/lib/worktree-safety.cjs',
'gsd-core/bin/lib/worktree-base-ref.cjs',
'gsd-core/bin/lib/planning-workspace.cjs',
'gsd-core/bin/lib/command-roster.cjs',
'gsd-core/bin/lib/runtime-artifact-conversion.cjs',
'gsd-core/bin/lib/runtime-artifact-layout.cjs',
'gsd-core/bin/lib/runtime-config-adapter-registry.cjs',
'gsd-core/bin/lib/runtime-hooks-surface.cjs',

29
src/command-roster.cts Normal file
View File

@@ -0,0 +1,29 @@
'use strict';
/**
* Command Roster Module
*
* Read-only helper for discovering canonical commands/gsd command stems and
* applying the shared GSD slash-command namespace transform.
*/
// eslint-disable-next-line @typescript-eslint/no-require-imports
const slashCommandTransformer = require('../../../scripts/fix-slash-commands.cjs') as {
readCmdNames: () => string[];
transformContentToHyphen: (src: string, cmdNames: string[]) => string;
transformContent: (src: string, cmdNames: string[]) => string;
buildPattern: (cmdNames: string[]) => RegExp | null;
buildColonPattern: (cmdNames: string[]) => RegExp | null;
};
function readGsdCommandNames(): string[] {
return slashCommandTransformer.readCmdNames();
}
export = {
readGsdCommandNames,
transformContentToHyphen: slashCommandTransformer.transformContentToHyphen,
transformContent: slashCommandTransformer.transformContent,
buildPattern: slashCommandTransformer.buildPattern,
buildColonPattern: slashCommandTransformer.buildColonPattern,
};

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,11 @@ const {
stageSkillsForRuntimeAsSkills,
stageCommandsForRuntimeFlat,
} = installProfiles;
// eslint-disable-next-line @typescript-eslint/no-require-imports
import runtimeArtifactConversion = require('./runtime-artifact-conversion.cjs');
const conversionExports = runtimeArtifactConversion as Record<string, unknown> & {
readGsdCommandNames?: () => string[];
};
// In .cts (CommonJS output) files, `require` is available as a global.
const _require: NodeRequire = require;
@@ -33,7 +38,6 @@ const _require: NodeRequire = require;
// ---------------------------------------------------------------------------
interface InstallExports {
readGsdCommandNames: () => string[];
computePathPrefix: (opts: { isGlobal: boolean; isOpencode: boolean; isWindowsHost: boolean; resolvedTarget: string; homeDir: string }) => string;
applyRuntimeContentRewritesInPlace: (stagedDir: string, runtime: string, pathPrefix: string) => void;
[converterName: string]: unknown;
@@ -194,8 +198,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string):
destSubpath,
prefix,
stage: (resolved) => {
const installExports = getInstallExports();
const buildKimiAgentArtifacts = installExports['buildKimiAgentArtifacts'] as (opts: {
const buildKimiAgentArtifacts = conversionExports['buildKimiAgentArtifacts'] as (opts: {
rootAgent?: string;
subagents?: Array<{ path: string; content: string }>;
}) => {
@@ -237,7 +240,7 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string):
*
* @param destSubpath
* @param prefix
* @param converterName name of converter function in bin/install.js exports
* @param converterName name of converter function in Runtime Artifact Conversion exports
* @param runtime canonical runtime ID (gates Hermes/Qwen branding in converter)
* @param configDir runtime config dir (for .gsd-source marker resolution)
* @param nested if true, nest concrete skills under their ns-* routers (#69)
@@ -260,15 +263,16 @@ function skillsKind(
destSubpath,
prefix,
stage: (resolved) => {
const installExports = getInstallExports();
const realConverter = installExports[converterName] as (content: string, skillName: string, runtime: string, cmdNames: string[], isGlobal: boolean) => string;
const realConverter = conversionExports[converterName] as (content: string, skillName: string, runtime: string, cmdNames: string[], isGlobal: boolean) => string;
// Compute cmdNames once per stage call for performance (#3583).
// Extra trailing args are ignored by converters that don't need them. The
// isGlobal flag is the 5th positional (NOT the 3rd): the 3rd positional is
// `runtime` for the claude/kimi/cline converters, so the scope-aware
// converters (antigravity, copilot) read isGlobal from position 5 to avoid
// colliding with `runtime` and always taking the global branch.
const cmdNames = installExports.readGsdCommandNames();
const cmdNames = conversionExports.readGsdCommandNames
? conversionExports.readGsdCommandNames()
: [];
const isGlobal = scope === 'global';
const wrappedConverter = (content: string, skillName: string): string =>
realConverter(content, skillName, runtime, cmdNames, isGlobal);
@@ -282,7 +286,7 @@ function skillsKind(
* commands directory with per-file conversion (e.g. Cursor 1.6 slash commands).
*
* Unlike `commandsKind` (which passes raw source files through), this kind
* applies `converterName` from bin/install.js exports to each file during
* applies `converterName` from Runtime Artifact Conversion exports to each file during
* staging, writing flat `${prefix}${stem}.md` files to the staged directory.
*
* The staged files are then written by `_copyStaged` (commands branch) which
@@ -290,7 +294,7 @@ function skillsKind(
*
* @param destSubpath destination subpath within configDir (e.g. 'commands')
* @param prefix filename prefix, e.g. 'gsd-'
* @param converterName name of converter function in bin/install.js exports
* @param converterName name of converter function in Runtime Artifact Conversion exports
* @param configDir runtime config dir (for .gsd-source marker resolution)
*/
function convertedCommandsKind(
@@ -304,8 +308,7 @@ function convertedCommandsKind(
destSubpath,
prefix,
stage: (resolved) => {
const installExports = getInstallExports();
const converter = installExports[converterName] as (content: string, commandName: string) => string;
const converter = conversionExports[converterName] as (content: string, commandName: string) => string;
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
},
};