enh(#2875): materialization primitives — durable user-artifact staging and descriptor-authoritative agents (#3600)

* fix(#2875): stage user artifacts durably across install wipes (#1874-F19)

preserveUserArtifacts held user files only in an in-memory Map across the
wipe, so any process death between preserve and restore lost them outright.

Seven call sites, not the four the issue records. Three of them never called
the helper at all - they open-coded the same read/wipe/write - so searching
for callers under-counted by construction; the extra sites were found by
sweeping for the pattern instead.

The worst is the mainline install path, where the crash window spans the
entire gsd-core tree copy rather than a single rmSync.

Adds src/user-artifact-staging.cts: durable on-disk staging with a record
written after the copies land as the commit point, plus recovery of orphaned
batches on the next run - without recovery the staged bytes survive but the
user's file is still gone, which would pass its own test while delivering
nothing.

Routes copyPreservingSymlink through installFs() so staging cannot bypass the
install fs seam, and reunites its symlink-safety docblock with the function it
documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): amend ADR-3574 with four claims disproved by implementation

Implementing Phase 6 disproved four statements the ADR rests on. The central
decision - no single materializer - is unaffected and stands.

Corrected: decision 3 was already satisfied, so nothing was extracted; the
agents-bypass runtime set omitted claude, kilo and opencode, and closing it
needed three new pieces of descriptor contract rather than proceeding on its
own terms; three of the four blockers the layout comment names were already
stale; and F19 is seven call sites, not four.

Records the generalizable lesson: the defect is the pattern of holding user
data in memory across a wipe, not the helper, so searching for callers of the
helper under-counts by construction.

Also resolves the ADR's open question on USER_OWNED_ARTIFACTS membership, and
notes that copyPreservingSymlink needed routing through the install fs seam
before it could be reused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close dangling-symlink blind spot and harden staging recovery

An adversarial review found the F19 staging work shipped red and unsafe.

Root cause, shared by two arbitrary-write findings: hasExistingSymlinkBetween
missed dangling symlinks in both its root check and its per-segment walk,
because it probed with existsSync, which is false for a link whose target does
not exist. Fixing only the new module would have reused a guard that was
itself blind. This guard protects the whole install tree.

Recovery no longer throws: it degrades per entry and per file, so one bad
batch cannot block the others. Previously an unrecoverable entry propagated
out of the first statement of install and uninstall, before the cleanup that
would have removed it - wedging the installer permanently.

Partial fs adapters now throw on any omitted method instead of silently
reaching the real filesystem, closing the trap that let a test poison list
pass while real IO happened.

Staged names must be flat, recovery refuses a dangling destination symlink,
and a batch whose recovery genuinely failed is no longer swept - it was
discarding the only durable copy of the file it had just failed to restore.

Replaces three tests that could not fail, including the one labelled negative
proof.

Known limitation, documented not closed: concurrent installs sharing a staging
key can still lose a batch. A real fix needs a cross-process lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* enh(#2875): make the descriptor authoritative for the agents kind

Deletes the inline agent-staging loop in bin/install.js and the
_DESCRIPTOR_AGENTS_RUNTIMES set, so every runtime materializes agents from
its capability descriptor instead of an inline hostBehaviors dispatch.

Closing it needed three pieces of contract the descriptor pipeline never had,
all reducible to one missing input - per-agent resolution context: a
frontmatter-extensions step for claude's effort and disallowedTools, per-agent
model-override resolution for kilo and opencode, and a named branding
converter for hermes, whose rewrite data was already declared.

Seven runtimes were on the loop, not the six the design recorded - kimi-code
was found by a golden fixture, not by analysis. claude-local and kimi-code
both silently lost their agents mid-change; the fixtures caught both and the
cause was fixed rather than the fixtures regenerated.

A parity harness gates the migration: both pipelines over identical inputs,
byte-identical output including filenames, per runtime. It is demonstrated
red before being trusted. Surface and install paths converge for all seven,
which also fixes surface previously writing no agents for these runtimes.

Codex's config.toml strip stays put - it mutates host config, which no
descriptor kind models.

Also routes install-model-override-resolver and install-effort-resolver
through the install fs seam. Both leaked real filesystem IO from the install
call tree; the stricter adapter is what exposed them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): record the agents-descriptor migration and correct the ADR count

The _DESCRIPTOR_AGENTS_RUNTIMES allow-list no longer exists, so the host
integration guide told readers to join a set that is gone. Replaces that with
what is now true - declare an agents entry and it installs, on the surface
path as well as install - and points anyone needing a per-agent transform at
the three extension points rather than at a new inline branch.

Corrects the ADR amendment: seven runtimes were on the inline loop, not six.
kimi-code was found by a golden fixture going red, not by reading. That is the
third short count this phase, all from enumerating by symbol or set membership
when the thing that matters is a behavior.

Adds the Changed changeset for the surface-path convergence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): amend ADR-2866 - claude global always wrote agents on disk

The claude row's global=[skills] described what capability.json declared, not
what the installer wrote. bin/install.js's inline agent-staging loop was never
scope-gated and never consulted the descriptor, so a claude --global install
has always written agents/gsd-*.md.

Phase 6 closes the gap by deleting that loop and declaring agents on claude's
descriptor at global scope. On-disk bytes are unchanged - the golden fixtures
did not move, which is the evidence that the descriptor, not the installer,
was incomplete.

#2218 is unaffected: agents are not trigger-bearing, so the wider row does not
introduce a new shadowing case.

Records the warning that an incomplete descriptor is invisible while a second
code path silently does its work, and only surfaces when the two are forced
into agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close review findings across staging, agents and the parity harness

Two independent reviews of this branch found defects the local gates missed.

Security: a dangling symlink at a migration destination allowed writing
outside configDir - the same class this change claimed to close, missed at the
terminal write of the flow being added. The staging-root resolver threw as the
first statement of install and uninstall, so a hostile symlink bricked both,
and symlinked-configDir users lost uninstall as well as install; it now
degrades instead of aborting. Recovery gained a source-side symlink check and
now refuses a relative destDir, which resolved against cwd. Converter dispatch
gained a runtime allowlist - lint-time validation stopped mattering once this
branch promoted that dispatch from the surface path to real installs.

Correctness: claude --local --minimal exited 1 because the minimal profile
legitimately yields zero agents and the new path treated that as a failure.
cline --local silently lost its agents - its descriptor declared none while
the deleted loop wrote them unconditionally. The agents prune was widened to
any gsd-* entry and destroyed user files it never owned.

The parity harness, on which the migration's safety argument rested, drove a
synthetic registry and never byte-compared the shipped descriptors; two of its
trap rows could not fail. It now drives the real registry across 13
runtime-scope rows including kimi-code and cline-local, and its red-proof is
demonstrated by corrupting a live capability.json. Three goldens that had
encoded the cline regression as expected behavior were corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): close findings from both mandated review engines

/security-review found the staging source-side walk honouring
GSD_ALLOW_SYMLINKED_DEST, an opt-in documented as relaxing only the write
destination. A symlinked files/ component dereferenced because
copyPreservingSymlink lstats the leaf only, so an intermediate link is
followed. The source walk no longer honours the opt-in; the destination check
still does.

/code-review spec axis found this branch had reintroduced its own bug:
migrateLegacyDevPreferencesToSkill's new symlink refusal threw unguarded after
the legacy dir was wiped and before the staged batch was restored, so a
planted symlink bricked uninstall permanently and orphaned the batch. Refusal
kept, abort removed.

kimi-code local silently lost its agents, the same class as the cline bug, and
the parity harness recorded that exclusion as intentional - the third test in
this branch to pin a regression as correct.

--minimal now creates an empty agents/ dir that never existed. Behaviour
restored rather than softening the changeset, so its byte-identical claim
stays true.

Standards axis: try/finally removed from twelve test bodies, fast-check
properties added for parseOwnerPid, boundary coverage at the grace window and
the ancestor-probe depth, a parity assertion for the staging-root helper
duplicated across two files, and the 8-deep config walk deduplicated.

Records 60-review.json with every finding and disposition from five passes,
including the smells left unfixed and why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2875): prune stale agents unconditionally in minimal mode

The previous round stopped an empty agents/ directory being created when the
resolved profile yields no agents. That was implemented by skipping the agents
kind entirely, which also skipped its stale-agent prune - so a full to minimal
downgrade left stale gsd-* agents behind.

The deleted inline loop pruned unconditionally and only skipped writing. Those
are three separate conditions, not one: prune always, write only when there is
something to write, create the directory only when writing.

Both call sites now run _removeGsdEntries before the empty-staged early exit.
The symlink-escape guard moved with it, since the prune also touches dest.
Codex .toml agents and the config.toml stanzas are cleaned again, and
user-owned agents are still preserved.

The agents/ directory is left in place after a prune empties it, matching
every sibling kind - none of them remove the destination directory itself.

Golden fixtures confirmed byte-identical: the prune is a no-op on a fresh
install, so fixture generation is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#2875): document interrupted-install recovery for user-owned files

The durable-staging fix is invisible to the user it protects. Someone whose
install died mid-flight has no way to know USER-PROFILE.md was staged before
the delete, that the next run restores it, or that recovery happens at the
start of that run rather than in the background.

Written as the task the user has - finish the interrupted command - rather
than as a description of the mechanism, and states what it will not do:
overwrite a file already present, or touch staging belonging to another
install still running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#2875): backfill changeset pr number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#2875): assert the J8 model override without building a regex

CodeQL flagged incomplete string escaping: the assertion interpolated the
override value into a RegExp while escaping only forward slashes, which is
meaningless in a constructor, leaving real metacharacters unescaped.

The failure direction was the dangerous one - a metacharacter would have made
the match more permissive, so the row would pass when it should fail. That
matters here because J8 exists precisely because an earlier revision was a
tautology; the rewrite reintroduced a different way for the same assertion to
stop discriminating.

Replaced with a line-wise exact match, so no regex is constructed at all.
Swept the other test files this branch adds; no sibling instances.

lint:ci passed on the original - lint-no-adhoc-regex-escape matches a full
metachar-escape copy, so a single slash replace slipped under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-08-17 17:25:53 -04:00
committed by GitHub
parent dfc4c69e3d
commit 3ab0007164
45 changed files with 5927 additions and 852 deletions

View File

@@ -37,6 +37,11 @@ import runtimeArtifactConversion = require('./runtime-artifact-conversion.cjs');
const conversionExports = runtimeArtifactConversion as Record<string, unknown> & {
readGsdCommandNames?: () => string[];
};
// #2875 Part 2 (J8): shared model-override precedence resolver — see its
// module doc for why kilo/opencode MUST resolve through this ONE function
// rather than re-deriving the chain per runtime.
// eslint-disable-next-line @typescript-eslint/no-require-imports
import installModelOverrideResolver = require('./install-model-override-resolver.cjs');
import { posixNormalize } from './shell-command-projection.cjs';
// #2870: `isGlobalScope` centralizes the `scope === 'global'` boolean
// projection both kind-builder closures below need at the converters'
@@ -91,6 +96,11 @@ interface AgentCtx {
runtime: string;
pathPrefix: string;
attribution: string | null | undefined;
/** #2875 Part 2 (row I1-I3): install root, threaded through to the
* frontmatter-extensions step and (for kilo/opencode's converters) the
* per-agent model-override resolution below. Mirrors install-profiles.cts's
* identically-named AgentCtx field — see its doc comment. */
targetDir?: string | null;
}
interface ArtifactKind {
@@ -255,14 +265,62 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art
// inline agent loop, and installCodexConfig's per-agent .toml writer. The
// exhaustive per-runtime sweep in tests/agent-fragments-emission.install.test.cjs
// is what keeps a fourth from appearing uncomposed.
stage: (resolved) => stageAgentsForRuntimeWithConverter(
// #2875 Part 2 (row I2): agentCtx threaded through so a runtime using this
// converter:null builder (claude, plus any future identity-copy runtime)
// ALSO gets path-rewrites/attribution/frontmatter-extensions/normalize
// when a caller supplies agentCtx (createRuntimeArtifactInstallPlan /
// applySurface's agentCtx build). Previously this closure's `(resolved) =>`
// signature silently dropped the second arg every caller already passed —
// a caller with NO agentCtx in scope is unaffected (row I2: converter-only,
// as today), matching stageAgentsForRuntimeWithConverter's own contract.
stage: (resolved, agentCtx) => stageAgentsForRuntimeWithConverter(
findAgentsSourceRoot(configDir),
resolved,
(content: string) => content,
false,
agentCtx,
),
};
}
/**
* Runtime allowlist check for a descriptor-declared `converter` name, applied
* at DISPATCH time (security fix). `VALID_CONVERTER_NAMES` (capability-
* validator.cjs) is otherwise enforced ONLY at lint/build time
* (`check:contract-drift`) — every `conversionExports[converterName]`
* dynamic-property read below trusted that a `capability.json` reaching this
* far had already passed that check. It had not, in general: a hand-edited
* or malformed descriptor naming an Object-prototype member (`"constructor"`,
* `"toString"`, `"hasOwnProperty"`, ...) resolves to that member instead of
* throwing, producing garbage staged content rather than a loud failure —
* pre-existing, but promoted from the `/gsd-surface`-only path to the real
* install path for seven runtimes by #2875 Part 2's agents-bypass closure.
* Required lazily (call-time, not module-top) to avoid a load-time circular
* require, the same pattern install-engine.cts's `_hostBehaviors` already
* uses for `capability-registry.cjs`. Fails CLOSED: any error loading the
* allowlist itself (missing module, exotic bundling) is treated as "nothing
* is allowed", never as "skip the check".
*/
function _resolveNamedConverter(converterName: string, kindLabel: string): (...args: unknown[]) => unknown {
let validNames: Set<string> | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
validNames = (require('./capability-validator.cjs') as { VALID_CONVERTER_NAMES: Set<string> }).VALID_CONVERTER_NAMES;
} catch {
validNames = undefined;
}
if (!validNames || !validNames.has(converterName)) {
throw new Error(
`Unknown converter "${converterName}" declared for a ${kindLabel} kind — refusing to dispatch (not in capability-validator.cjs's VALID_CONVERTER_NAMES allowlist).`,
);
}
const fn = conversionExports[converterName];
if (typeof fn !== 'function') {
throw new Error(`Converter "${converterName}" is allowlisted but is not an exported function of runtime-artifact-conversion.cjs.`);
}
return fn as (...args: unknown[]) => unknown;
}
/**
* Build a converted-agents kind descriptor for runtimes whose agent `.md` files
* need runtime-specific frontmatter/body conversion (e.g. Copilot, Cursor, Codex).
@@ -274,27 +332,80 @@ 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 (real install still elsewhere): this provides
* the converter dispatch + `isGlobal` scope threading for the descriptor's
* `agents` kind. As of #2092, 8 non-Claude runtimes DO declare a converted
* `agents` kind in their `capability.json` — qwen (`convertClaudeAgentToQwenAgent`)
* plus the 7 that already declared one before it (antigravity, augment,
* codebuddy, copilot, cursor, trae, windsurf) — so the descriptor-level
* declaration is no longer deferred. What IS still deferred is wiring
* `resolveRuntimeArtifactLayout`'s `agents` kind into the REAL install:
* `bin/install.js`'s agent-staging loop does not consume this module's
* `convertedAgentsKind` resolution at all — it dispatches the very same
* converter functions directly via `_hostBehaviors(runtime)` checks
* (`frontmatterDialect`, `brandingRewrites`, `isCopilot`/`isAntigravity`/…),
* duplicating the mapping declared here. That duplication is deliberate until
* the second `layout.kinds` consumer — `applySurface` / `/gsd:surface` /
* `--materialize` (`src/surface.cts`) — mirrors the legacy agent pipeline
* (Copilot's `.agent.md` filename rename, the cross-cutting path-prefix
* rewrite + attribution, stale-file cleanup, config-reading steps); declaring
* `bin/install.js` itself against this resolver before then would risk
* regressing the surface path. Until that follow-up lands, `bin/install.js`
* remains authoritative for the real install, and this `convertedAgentsKind`
* is exercised only by `/gsd:surface` and synthetic-descriptor seam tests.
* #1173 SCOPE, updated by #2875 Part 2 (the agents-bypass closure) — measured
* against the tree, not the ADR-3574 framing that preceded it:
*
* Of the four blockers this comment used to name for wiring `bin/install.js`'s
* inline agent loop against this resolver, THREE were already stale by the
* time #2875 measured them and are not re-litigated here: Copilot's
* `.agent.md` rename (the loop's own `destName = entry.name` comment records
* the ternary dropped in #2099; the descriptor fold applies it via
* `hostBehaviors.agentFileExtension`), the cross-cutting path-prefix rewrite +
* attribution (`stageAgentsForRuntimeWithConverter` already applies
* `applyAgentPathRewrites` -> `processAttribution` when `agentCtx` is
* present), and stale-file cleanup (`_removeGsdEntries` prunes every
* `gsd-`-prefixed entry in a kind's destSubpath, broader than the loop's own
* extension-gated check).
*
* The fourth — config-reading steps — was the real gap, and #2875 Part 2
* closed it: `stageAgentsForRuntimeWithConverter` now takes a per-file
* `agentName` (`agentCtx.agentName`, ADR-1235 §1) and a `targetDir`
* (`agentCtx.targetDir`), which together let it (a) run a post-converter
* frontmatter-extensions step (`applyAgentFrontmatterExtensions`, driven by
* `hostBehaviors.agentFrontmatterExtensions` — Claude's `effort` +
* `disallowedTools` injection) and (b) let THIS function resolve a per-agent
* model override (`installModelOverrideResolver.resolveAgentModelOverride`,
* `model_overrides[agent]` > `model_profile_overrides.<rt>.<tier>` > omit)
* before invoking a converter that needs it (kilo/opencode). Both pieces —
* plus a data-driven Hermes branding converter
* (`convertClaudeAgentToHermesAgent`, reading `hostBehaviors.brandingRewrites`
* rather than a hardcoded string table) — are single-sourced: `bin/install.js`
* requires the SAME functions this module does, so its inline loop and the
* descriptor path can no longer independently drift (the CLAUDE.md
* "Generative Fix Divergence" class the prior duplication risked).
*
* `tests/agent-descriptor-parity.test.cjs` proves byte-identical output
* between the inline loop and a SYNTHETIC descriptor registry (the same
* override seam `resolveRuntimeArtifactLayoutFromRegistry` exposes) for all
* six runtimes the inline loop still served: claude, cline, codex, hermes,
* kilo, opencode.
*
* Both findings the prior revision of this comment named as STILL deferred
* are now CLOSED (#2875 Part 2 Task A/B/C), measured against the real
* `capability.json` entries and the real production entry points, not
* argued from this module alone:
*
* 1. **kilo/opencode reaching `layout.kinds`.** `installEngine.
* installAgentsKindStandalone` (install-engine.cts) is called from inside
* `installOpencodeFamilyArtifacts` and resolves the agents kind through
* THIS SAME `resolveRuntimeArtifactLayout`/`convertedAgentsKind` path —
* `installOpencodeFamilyArtifacts` no longer stages only `commands` +
* `skills`. `bin/install.js`'s legacy-flat local path (claude-local,
* `hostBehaviors.localInstallStyle === 'legacy-flat'`) reaches the SAME
* generic loop only via `installRuntimeArtifacts`'s conditional
* `_isSkillsRuntime` branch; a call to `installAgentsKindStandalone` was
* added at claude-local's own call site to cover that scope too — the
* install-tree golden fixture (`tests/fixtures/install-tree/claude-local.json`)
* is what caught the gap when it was first missed.
* 2. **`/gsd:surface` / `applySurface` activation.** Confirmed convergent,
* not merely non-broken: for all six runtimes (claude, cline, codex,
* hermes, kilo, opencode), staging via `applySurface` into a freshly
* wiped `agents/` directory produces byte-identical output (including
* filenames) to `installRuntimeArtifacts`'s own write — verified directly
* against the built registry, not inferred.
*
* The inline loop (`_DESCRIPTOR_AGENTS_RUNTIMES` and the `bin/install.js`
* agent-staging block it gated) is DELETED — every runtime the registry
* declares an `agents` kind for is descriptor-driven now, including a
* seventh runtime (`kimi-code`) this comment's own prior measurement missed
* (it fell through the inline loop's generic `else if` branch, same as
* claude, with no dedicated dialect arm — caught by the same golden fixture).
*
* Codex's `config.toml [agents.gsd-*]` strip (`bin/install.js`, under
* `isMinimalMode` + `hostBehaviors.tomlConfigInstall`) remains the one
* genuinely out-of-scope constraint: it mutates a host config file, not the
* agents directory, and no descriptor kind models host-config mutation. It
* stays exactly where it is.
*
* Mirrors the `convertedCommandsKind` pattern (#785).
*
@@ -315,16 +426,42 @@ function convertedAgentsKind(
destSubpath,
prefix,
stage: (resolved, agentCtx) => {
// 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).
// #2870: `scope` is this function's own parameter (default `'global'`,
// so it is never undefined here), sourced upstream from the Install
// Scope Module's resolved id. `isGlobalScope` projects it to the
// boolean `stageAgentsForRuntimeWithConverter`'s positional API
// requires — see its doc comment in install-scope.cts.
const converter = conversionExports[converterName] as (content: string, isGlobal?: boolean) => string;
const rawConverter = _resolveNamedConverter(converterName, 'agents') as
(content: string, arg2?: boolean | { isAgent?: boolean; modelOverride?: string | null }) => string;
// #2875 Part 2 (J5-J8): kilo/opencode agent converters take an options
// bag (`{isAgent, modelOverride}`), not the `isGlobal` boolean every
// other agent converter's 2nd positional arg means — mirrors the
// inline loop's per-runtime `frontmatterDialect === 'opencode' | 'kilo'`
// branches (bin/install.js), which resolve model_overrides[agent] >
// model_profile_overrides.<runtime>.<tier> > omit BEFORE calling the
// converter. Resolved ONCE per stage() call (not per file — a pure
// function of configDir/targetDir) via the single shared precedence
// resolver so kilo and opencode can never diverge (J8).
const needsModelOverride = converterName === 'convertClaudeToOpencodeFrontmatter' || converterName === 'convertClaudeToKiloFrontmatter';
let converter: (content: string, isGlobal?: boolean, meta?: { agentName: string }) => string;
if (needsModelOverride) {
const overrideTargetDir = agentCtx?.targetDir ?? configDir;
const modelOverrides = installModelOverrideResolver.readGsdEffectiveModelOverrides(overrideTargetDir);
const runtimeResolver = installModelOverrideResolver.readGsdRuntimeProfileResolver(overrideTargetDir);
converter = (content, _isGlobal, meta) => {
const modelOverride = meta
? installModelOverrideResolver.resolveAgentModelOverride(meta.agentName, modelOverrides, runtimeResolver)
: null;
return rawConverter(content, { isAgent: true, modelOverride });
};
} else {
// 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).
converter = (content) => rawConverter(content, isGlobalScope(scope));
}
// ADR-1235 §1: when agentCtx is provided (by createRuntimeArtifactInstallPlan
// for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
// can apply the full pre-converter + post-converter sequence in the correct order.
@@ -422,7 +559,7 @@ function skillsKind(
prefix,
converter: converterName,
stage: (resolved) => {
const realConverter = conversionExports[converterName] as (content: string, skillName: string, runtime: string, cmdNames: string[], isGlobal: boolean) => string;
const realConverter = _resolveNamedConverter(converterName, 'skills') 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
@@ -481,7 +618,7 @@ function convertedCommandsKind(
destSubpath,
prefix,
stage: (resolved) => {
const converter = conversionExports[converterName] as (content: string, commandName: string) => string;
const converter = _resolveNamedConverter(converterName, 'commands') as (content: string, commandName: string) => string;
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
},
};