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:
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user