Append a dated amendment to ADR-1508 (append-only convention) recording that the Runtime Artifact Conversion Module decision is implemented on next: the content-rewrite engine + walkers + computePathPrefix live behind the deep seam rewriteStagedSkillBodies/rewriteStagedCommandBodies, the getInstallExports relay and GSD_TEST_MODE require are deleted, and CONTEXT.md marks the module SHIPPED. Status stays Accepted (no Implemented status per docs/adr/README.md). Two deferred follow-ups are tracked as sub-issues of the epic (neither a blocker): - #1675 dedup convertClaudeToAugmentMarkdown family - #1676 fast-check property test ($HOME-collapse + idempotency) Delivery verified by a Codex (gpt-5.4, high) read-only review against the epic's stated deliverables, cross-checked against the indexed code graph. Closes #1507
12 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. Exception: opencode and kilo path-prefix rewriting remains a deliberatebin/install.js-owned pre-conversion step (seeapplyOpencodeFamilyPathPrefix); this is intentional per #784 and is not a violation of the single-owner rule. - 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).
Amendment — 2026-06-24: Implementation complete (epic #1507 closed)
The decision recorded above is implemented on next. The ADR Status stays Accepted — per this directory's append-only convention there is no "Implemented" status; this dated amendment records that the decision is realized.
Landed:
- Phase 1 (#1512):
getDirName→src/runtime-name-policy.cts;processAttribution→src/runtime-artifact-conversion.cts. (getCommitAttributionstays inbin/install.js— a documented scope refinement: it is impure install-time config I/O, not a content-transformation helper, so it cannot move into the pure conversion module. Phase 2 injects the resolved attribution value instead.) - Phase 2 (#1513): the content-rewrite engine (
_applyRuntimeRewrites), both staged-content walkers, andcomputePathPrefix(private;_computePathPrefixfor tests) live insrc/runtime-artifact-conversion.ctsbehind the deep seamrewriteStagedSkillBodies/rewriteStagedCommandBodies({runtime, configDir, scope, homedir?, platform?, resolveAttribution?}). ThegetInstallExports/loadInstallExports/InstallExportsrelay and theGSD_TEST_MODEinstall.jsrequireare deleted fromsrc/runtime-artifact-layout.cts— the last upward.cts → bin/install.jsdependency is gone.CONTEXT.mdmarks the module SHIPPED. (The install-side cutover lands one indirection deeper than the literal issue text —install.jsdelegates tocreateRuntimeArtifactInstallPlan, which performs the deep calls insrc/runtime-artifact-install-plan.cts— satisfying the same dependency-direction intent with a cleaner owner.)
Two deferred follow-ups were spun out as sub-issues of #1507 and have since been delivered (neither was a blocker; the architectural goal — single ownership, downward dependency direction, relay deletion — was already met by the merged slices above):
- #1675 (PR #1685) — deduped the byte-identical
convertClaudeToAugmentMarkdown/convertSlashCommandsToAugmentSkillMentionsbetweenbin/install.jsand the conversion module (the Phase 1 → Phase 2 deferred cleanup; install.js now binds them from the conversion module, single-sourced). - #1676 (PR #1686) — added the
fast-checkproperty test ($HOME-collapse invariant + path-rewrite idempotency) promised in #1511's test scope.
Delivery verified by a Codex (gpt-5.4, high-effort, read-only) review against the epic's stated deliverables, cross-checked against the indexed code graph and live source.