Phase 0 of epic #1507. Records the decision to make the Runtime Artifact Conversion Module the single owner of per-runtime content rewriting, flip the dependency direction to installer/layout -> conversion, and close the surface.cts -> bin/install.js getInstallExports relay. Doc-only. Resolves ADR-3660 Initial-Scope deferral; distinct from epic #1258. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187qgypdy1wkWRpdaf2hRuD
9.2 KiB
Runtime Artifact Conversion Module owns per-runtime content rewriting
- Status: Accepted
- Date: 2026-06-20
- Issue: #1508
- Epic: #1507
- Implementation: Phase 1 (helper relocation, no behavior change) → Phase 2 (engine move + relay deletion)
The Runtime Surface Module (src/surface.cts → surface.cjs) re-materializes a resolved skill surface to disk via applySurface. For skills kinds it must rewrite staged SKILL.md bodies so their @-ref paths point at the install target (pathPrefix) instead of the converter's default ~/.claude paths (#813). To do that it reaches up into the 12,289-line hand-authored bin/install.js via getInstallExports() (src/runtime-artifact-layout.cts:53-69) — a lazy require('../../../bin/install.js') guarded by a save/set/restore of GSD_TEST_MODE — to borrow computePathPrefix and applyRuntimeContentRewritesInPlace.
This is the last upward dependency from the .cts source tree into the hand-authored installer. It forces an env-var dance at a test seam, and it leaks: applySurface (and bin/install.js's own three call sites) each re-derive the same five path-prefix inputs (scope→isGlobal, runtime==='opencode', process.platform, normalized resolvedTarget, normalized homeDir) before calling computePathPrefix. The prefix-derivation knowledge is duplicated across surface.cts and install.js.
CONTEXT.md already names the Runtime Artifact Conversion Module (src/runtime-artifact-conversion.cts) as the [Planned] sibling of the Layout Module — placement vs. content. ADR-3660 §Initial Scope deferred exactly this consolidation: "A future ADR may consolidate them into a Skill Conversion Module if a second consumer emerges." surface.cts is that second consumer. This is that future ADR.
Decision
- Promote the
[Planned]Runtime Artifact Conversion Module (src/runtime-artifact-conversion.cts) to the single owner of per-runtime content rewriting: the per-runtime converters (already relocated as ADR-3660's "first slice", #1099), plus the rewrite engine_applyRuntimeRewrites, the staged-content walkers, path-prefix derivation, and commit attribution. The Runtime Artifact Layout Module keeps owning placement only. - Public seam — two deep calls; the caller passes only what it has, the module derives the rest:
rewriteStagedSkillBodies(stagedDir, { runtime, configDir, scope }, env?)— in-place walk (skills / kimi-agents).rewriteStagedCommandBodies(stagedDir, { runtime, configDir, scope }, env?) → tempDir— copy-to-temp (commands).- The module internally derives
isGlobal/isOpencode/isWindowsHost/resolvedTarget/homeDirand the path prefix.env = { homedir = os.homedir, platform = process.platform } = {}is an injected test seam (the clock-seam analog,RULESET.TESTS.clock-seam).
computePathPrefixbecomes private to the module, exported as_computePathPrefixfor direct unit +fast-checkproperty tests (RULESET.TESTS.property-based-testing). The hand-reimplemented copy intests/path-replacement.test.cjsis deleted so the real function is what's tested (it is effectively untested today).- Dependency direction:
bin/install.jsandruntime-artifact-layout.ctsimport the conversion module; the conversion module imports nothing upward (notinstall.js, notlayout) — only deeper leaves. getDirName(runtime)relocates tosrc/runtime-name-policy.cts(a cleanfs/path-only leaf), so the conversion module can consume it without dragging incapability-registry.cjs(whichruntime-homes.cjsrequires).processAttribution/getCommitAttributionmove into the conversion module (attribution is content transformation).- The duplicate
convertClaudeToAugmentMarkdown(verified byte-identical ininstall.js:2584andconversion.cts:976) collapses to the conversion-module copy;install.js's local copy is deleted (it already re-exports...runtimeArtifactConversion). getInstallExports/loadInstallExports/ theInstallExportsinterface and theGSD_TEST_MODErequire ofbin/install.jsare deleted fromruntime-artifact-layout.cts.surface.cts(the sole consumer) calls the conversion module's deep functions directly — removing the last upward.cts → install.jsdependency.
Initial Scope
Phase 1 — helper relocation (no behavior change)
- Move
getDirName→runtime-name-policy.cts; re-point its 13install.jscall sites. - Move
processAttribution+getCommitAttribution→conversion.cts; re-point their 21install.jscall sites. - Delete
install.js's localconvertClaudeToAugmentMarkdown(copies confirmed byte-identical); rely on the conversion-module copy via the existing...runtimeArtifactConversionexport spread. Add a characterization test snapshotting current augment skills-rewrite output as insurance — it should pass unchanged. - No public-interface change;
install.jsandsurface.ctsbehavior unchanged.
Phase 2 — engine move + deepen + delete relay
- Move
_applyRuntimeRewrites,applyRuntimeContentRewritesInPlace,applyRuntimeContentRewritesForCommandsInPlace, andcomputePathPrefixintoconversion.cts. - Expose
rewriteStagedSkillBodies/rewriteStagedCommandBodies; privatizecomputePathPrefix(_computePathPrefixfor tests). surface.cts:applySurfaceandinstall.js's three internal sites (7261/7276,9475) call the deep functions; delete the per-site prefix derivation.- Delete
getInstallExports/loadInstallExports/InstallExports+ theGSD_TEST_MODEbin/install.jsrequire fromruntime-artifact-layout.cts. - Tests:
fast-checkproperty test for the rewrite engine ($HOME-collapse invariant; path-rewrite idempotency), direct_computePathPrefixunit tests, delete thepath-replacement.test.cjsreimplementation, and aDEFECT.GENERATIVE-FIXparity guard ensuring no second converter copy reappears.
These phases should NOT
- Bundle ADR-3660 Phase 2 (install/uninstall
layout.kindsloop collapse, ~250 lines, separate issue #3664). - Relocate
getConfigDirFromHomeor other general install helpers the rewrite engine does not need.
Migration Inventory
New files
docs/adr/1508-runtime-artifact-conversion-module.md(this ADR) + README index row.CONTEXT.mdglossary: flip Runtime Artifact Conversion Module[Planned]→ shipped, and update the Runtime Artifact Layout Module entry (thegetInstallExportsseam sentence is removed). (lands with Phase 2)
Phase 1 modified
src/runtime-name-policy.cts—+getDirName.src/runtime-artifact-conversion.cts—+processAttribution,+getCommitAttribution.bin/install.js— re-point 13 (getDirName) + 21 (attribution) call sites; delete localconvertClaudeToAugmentMarkdown.- tests — augment characterization test.
Phase 2 modified
src/runtime-artifact-conversion.cts—+_applyRuntimeRewrites,+both walkers,+computePathPrefix(private) + deep seam.src/surface.cts— deep-call cutover; drop thegetInstallExportsimport + prefix math.src/runtime-artifact-layout.cts— deletegetInstallExports/loadInstallExports/InstallExports+ theinstall.jsrequire.bin/install.js— three sites call the deep functions; import them back from the conversion module.- tests — engine property test,
_computePathPrefixunit tests, deletepath-replacement.test.cjsreimplementation, parity guard.
Consequences
- +
surface.ctsandinstall.jsstop re-deriving the path prefix — one owner, leak dissolved at both sites. - + The
.ctssource tree no longer reaches into hand-authoredbin/install.js;runtime-artifact-layout.ctsno longer requiresinstall.jsor togglesGSD_TEST_MODE. - +
computePathPrefixgains real unit + property coverage it lacks today. - −
bin/install.jsstays hand-authored JS; it now imports the rewrite engine back from the generatedconversion.cjs— the same pattern it already uses forhooksSurfaceand...runtimeArtifactConversion. Only the moved functions become TypeScript;install.jsitself is not converted. - − Two-phase sequence; CONTEXT.md glossary, ADR README index, and
lint:ci(ADR-HEADER) updates required at merge.
Relationship to other ADRs and issues
- ADR-3660 (Runtime Artifact Layout Module): resolves its §Initial Scope deferral ("A future ADR may consolidate them … if a second consumer emerges"). Layout owns placement; this module owns content. Independent of ADR-3660 Phase 2 (#3664).
- ADR-457 (generated-CJS single source): the moved engine is authored in
src/*.ctsand consumed as generatedbin/lib/*.cjs, consistent with the single-source rule. - ADR-1235 (descriptor-driven agent conversion): complementary — both narrow
bin/install.js's ownership of conversion concerns. - Epic #1507 tracks the phases. Distinct from epic #1258 (cross-runtime skill mapping + plugin skill provision/consumption): #1258 Phase A documents the converter transform-contract catalog; this ADR decides module ownership + dependency direction + engine relocation. Continues #1099 (closed first slice that created the module) and is a sibling of #1173 (agent-converter wiring).