* chore(#4727): name the tool-conversion helpers for the runtime that uses them
GSD has had no Gemini runtime since #1928 removed it (Google sunset Gemini CLI
on 2026-06-18, shipped 1.8.0), yet two helpers were still named for it:
claudeToGeminiTools -> claudeToAntigravityTools
convertGeminiToolName -> convertAntigravityToolName
The sole consumer is convertClaudeAgentToAntigravityAgent, whose own comment
read "Map tools to Gemini equivalents (reuse existing convertGeminiToolName)".
Nothing named Gemini consumes them, because nothing named Gemini exists. The
new names follow the convention the file already sets with its neighbouring
Copilot pair, claudeToCopilotTools / convertCopilotToolName.
Zero behavior change. Every mapped VALUE is byte-identical, deliberately:
read_file, write_file, replace, run_shell_command, glob,
search_file_content, google_web_search, web_fetch, write_todos
Those are Gemini's built-in tool dialect and Antigravity genuinely speaks it.
This rename covers only the identifiers, which are the one part of the surface
that was GSD's choice rather than Google's contract.
Renamed in BOTH copies. CLAUDE.md labels bin/install.js "(generated)", but no
script emits it -- build:lib is tsc -p tsconfig.build.json and writes only
gsd-core/bin/lib/**. These converters are the #1099/#1173/#1182 situation: they
were extracted into src/runtime-artifact-conversion.cts while bin/install.js
kept its own working inline copies, so each symbol existed twice in two
independently hand-maintained files. Renaming one would have left two names for
one concept. Verified first that no capability descriptor resolves either by
name -- antigravity's descriptor names only convertClaudeCommandToAntigravitySkill
and convertClaudeAgentToAntigravityAgent, neither of which moved.
Comments keep their reasoning and their issue refs (#3362 AskUserQuestion,
#1394 Skill/SlashCommand); only the subject is corrected, from "Gemini CLI" to
Antigravity speaking the Gemini dialect. Those describe the dialect's behavior,
which is still Antigravity's behavior, so deleting them would destroy the record
of two real bugs.
docs/research/gemini-to-antigravity-migration.md is left unedited and carries a
dated addendum instead: it is pinned to c0b2a05d2f and quotes #1928's commit
message verbatim, so rewriting a citation to match a later tree would falsify a
primary source. ADR-1593's dimension-3 row is updated, because that table is a
present-tense index of which helper implements each dimension and would
otherwise name a symbol that no longer exists.
Coverage: the module is the only export surface -- bin/install.js exports none
of these four, its inline copies being module-private -- so the new assertion
targets gsd-core/bin/lib/runtime-artifact-conversion.cjs and checks the new keys
present, the old keys absent (no alias left behind), deepStrictEqual on the whole
map so an added or removed key fails, and each excluded input individually. The
installer's inline copy stays covered behaviorally by the existing test that
imports convertClaudeAgentToAntigravityAgent from bin/install.js.
Phase 4a of epic #4709. Refs #4727
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4727): fix three review blockers — ADR append-only, honest verdict, changeset
The isolated adversarial review returned BLOCK on three majors. All three were
right and all three are fixed here.
1. ADR-1593 was amended IN PLACE, violating docs/adr/README.md:5: "ADRs are
append-only. Amendments extend existing ADRs with a dated section rather
than replacing them." The dimension-3 row is restored to its original text
and a dated "Amendment — 2026-09-14 (#4727)" section is appended instead.
Worse than the policy breach: the previous commit applied OPPOSITE rules to
two docs in one change, freezing the research doc's citations while
rewriting the ADR's. Both now follow append-only.
2. The research-doc addendum claimed "the rename recommended in the PRESERVE
table has landed" and that the "PRESERVE — trap" verdict "still holds".
Neither is true. §2(c) is headed "MUST NOT be renamed" and the §6 table
files both symbols under (c). The rename OVERTURNS that verdict, and the
addendum now says so in those words: it is a NARROWING of (c), whose real
subject is what Google owns -- the directories, the dialect, GEMINI.md, the
model ids, and for these two symbols the mapped VALUES, which stay
byte-identical. What (c) had swept in with them were two GSD-chosen
identifiers no external contract references. Claiming endorsement from a
verdict that forbade the change was the actual defect; the substantive
argument was always sound.
The addendum also now names the two superseded rows (~:55, ~:136) so a
later reader is not misled, and discloses that §2(a)'s verbatim quotation
(~:40) no longer matches its source: it reproduces the test docblock's old
"convertGeminiToolName" wording, and #4727 reworded that docblock. The
docblock had to move -- leaving it would make the #1928 guard describe a
symbol that does not exist -- so the mismatch is disclosed rather than
papered over by editing the quote, which is the one thing the addendum
exists to avoid.
3. no-changelog was the wrong call. scripts/changeset/lint.cjs reports
fail_missing_fragment because the diff touches src/ and bin/, and
CONTRIBUTING.md:216 states src/ edits are user-facing "even though the
generated .cjs is gitignored", with :224 adding "When unsure whether a
change is user-facing, add the fragment." Confirmed concretely: gsd-core is
in package.json's files array so the compiled module ships, and there is no
exports map, so a consumer's deep require of
gsd-core/bin/lib/runtime-artifact-conversion.cjs resolved
.convertGeminiToolName before this change and gets undefined after. A
Changed fragment is added. I had asserted "nothing user-facing" without
running the repo's own changeset lint; the reviewer ran it.
Two review findings are accepted and recorded rather than fixed, both in the
research addendum's new §8: the bin/install.js half of the rename is
test-unprotected (that file exports none of the four identifiers and the export
audit asserts undefined for both spellings, so reverting it breaks no test --
its behavior is covered, its naming is not), and closing that needs the
repo-wide drift guard, which is #4729 and the last phase of the epic for
precisely this reason.
Refs #4727
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#4727): backfill changeset pr number to 4732
Refs #4727
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(#4727): assert both halves of the windsurf pre-write guard's contract
PR #4732's `conformance test (windows-latest, 24, shard 2/3)` failed on exactly
one assertion, and the base branch was green, so this is not inherited:
tests/windsurf-hooks-bridge.test.cjs:104
expected exit 2, got 0
stderr: gsd-windsurf-pre-write: git probe 'git rev-parse --show-toplevel (cwd)' …
duration_ms: 2909
Root cause, not a flake. hooks/gsd-windsurf-pre-write.js gives every git probe a
2000 ms budget (SPAWNOPT.timeout) and FAILS OPEN when a probe cannot run — its
own header states that outright, because "a hook bug must never wedge Cascade".
hooks/lib/git-probe.js (#3911) exists precisely because that budget is routinely
exceeded in CI; its header records a macOS run landing at 2084/2112/2177 ms,
just past the budget, and its reportIfUndetermined() announces the case on
stderr while deliberately changing no exit code.
So the hook has TWO documented outcomes:
(a) probe resolved and the file's git root differs -> BLOCK, exit 2
(b) probe UNDETERMINED (timeout / spawn failure) -> FAIL OPEN, exit 0,
announced on stderr
G1 and G1b asserted `status === 2` unconditionally, encoding only (a). They
therefore fail whenever the documented (b) occurs, which makes them
load-sensitive by construction. My diff did not touch that hook or its tests;
it re-packed the Windows chunks (6 -> 8 under #4737's derived cap), which raised
load enough to tip the 2 s budget and expose the latent assertion.
Both tests now branch on whether the hook ANNOUNCED an undetermined probe, and
assert the correct half in each case. This is not a loosened assertion:
- undetermined -> exit 0 is REQUIRED. That arm still has teeth, because it
fails if the hook ever blocks on a probe it could not determine, which is
the dangerous direction — wedging the agent on a hook bug.
- determined -> the original exit 2 plus the original stderr-reason regex,
unchanged.
The matcher is tied to reportIfUndetermined's exact message rather than a loose
/git probe/, and was proven against both strings: it matches the real
undetermined diagnostic and does NOT match a normal block message. No retry, no
sleep, no timing-dependent logic.
Deliberately NOT done: raising the hook's 2000 ms budget. That is forbidden here
without an explicit instruction, and it would only move the cliff rather than
fix the test's false premise.
Scope note: this is a test fix in a file unrelated to the rename, carried here
because it is what makes #4732 red. CLAUDE.md's no-deferral rule is explicit
that a defect found anywhere in the tree is fixed in the current change, and
that it overrides one-concern-per-PR.
Refs #4727
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Skill mapping & converter methodology across runtimes
- Status: Accepted
- Date: 2026-06-22
- Issue: #1593
- Epic: #1258 (Phase A — "do first")
- Extends: ADR-3660 (layout), ADR-1016 (enum — accepted here)
- Sibling: ADR-1508 (module ownership), ADR-766 (Claude plugin manifest)
Context
GSD installs skills into 16 host CLIs (claude, codex, gemini, opencode, kilo, cursor, copilot, antigravity, windsurf, augment, trae, qwen, hermes, codebuddy, cline, kimi). The methodology governing how a source command file in commands/gsd/*.md becomes an installed skill on each runtime is real, load-bearing, and documented in fragments across three sources that disagree on what they own:
- ADR-3660 (Accepted) — owns the structural layout: the
{ kind, destSubpath, prefix, nesting, recursive, converter }ArtifactKindDescriptorshape, per-runtime dest path, thegsd-prefix, flat-vs-nested undergsd-ns-*routers, and thestageclosure contract binding each layout to its converter. ADR-3660 says where artifacts go; it does not describe what the converters do. - ADR-1016 (header Status: Proposed — corrected to Accepted by this ADR, see Decision 2) — owns the closed
ConverterNameenum and declaresartifactLayoutas descriptor data. Vocabulary only: it closes the set of named converters; it does not describe each converter's transform contract. src/runtime-artifact-conversion.cts(~2,600 lines, the converter functions) — the actual per-runtime transform semantics: frontmatter filtering, tool-name rewrites, path rewrites, namespacing, description truncation, SKILL.md-vs-flat body format. No ADR. A future maintainer (human or agent) has no single place that says "this converter rewrites X, drops Y, truncates at Z."
A just-merged sibling — ADR-1508 (PR #1509, 2026-06-21) — owns module ownership + dependency direction for the conversion engine. Its body explicitly defers the methodology to this ADR: "Distinct from epic #1258: #1258 Phase A documents the converter transform-contract catalog; this ADR decides module ownership + dependency direction." The module now has a home; the methodology it implements did not.
Two concrete failures fall out of this documentation gap (surfaced while triaging #1243):
- Consumption:
agent_skills'sglobal:resolver hand-resolved a file path and could not reach plugin-provided skills. The resolution (PR #1261, Claude consume side) had to reverse-engineer the converter + layout + the platform's native skill-resolution mechanism separately because no ADR described how they relate. - Provision: GSD ships as a first-party plugin/extension on multiple platforms (
.claude-plugin/plugin.jsonper ADR-766,gemini-extension.jsonper #775), but those manifests do not provide GSD's skills the platform-native way — the Claude manifest declarescommands+hooks, noskills. No ADR states the provision methodology each platform demands.
Decision
1. This ADR is the single authoritative description of the per-runtime skill mapping and converter transform contracts
It codifies — in one place — what ADR-3660 (layout), ADR-1016 (vocabulary), and runtime-artifact-conversion.cts (semantics) each carry a third of. The companion reference page, docs/reference/skill-mapping-matrix.md, holds the maintainable per-runtime table; this ADR holds the decisions behind it. References, does not duplicate, ADR-3660 (the layout owner) — extends it with the converter + mapping methodology.
2. ADR-1016's ConverterName enum is Accepted (header correction)
ADR-1016's header says Proposed, but its ConverterName closed enum is already code-enforced: gsd-core/bin/lib/capability-validator.cjs rejects unknown converter names ("is not a known ConverterName"), and the enum is locked by a fail-first regression test at tests/capability-registry.test.cjs:3956 (ADR-857 phase 5e). The decision is realized; the record is stale. This ADR accepts the enum and the ADR-1016 header is corrected Proposed → Accepted as a metadata correction (no behavior change).
The closed enum VALID_CONVERTER_NAMES (capability-validator.cjs:651-678) holds 24 names in two blocks:
- 15 commands/skills converters — the block ADR-1016's "15 named first-party functions covering the 16 runtimes" refers to. Of these, 13 are skill converters and 2 are command converters (
convertClaudeCommandToCodebuddyCommand,convertClaudeCommandToCursorCommand). Three runtimes shareconvertClaudeCommandToClaudeSkill(claude, qwen, hermes), so the 15 skill-bearing runtimes (all except commands-only Gemini) resolve to 13 distinct skill converters. - 9 agent converters (
convertClaudeAgentTo{Copilot,Antigravity,Cursor,Windsurf,Augment,Trae,Codebuddy,Cline,Codex}Agent) — added by #1173 for the descriptor-driven agent-conversion wiring (ADR-1235). These are not yet declared by any runtime'sagentskind descriptor (theconvertedAgentsKindbuilder exists but the declarations are deferred to a #1173 follow-up; the legacybin/install.jsagent loop remains authoritative).
3. The converter transform-contract categories
Every skill converter in runtime-artifact-conversion.cts composes some subset of eight transform categories. This is the catalog ADR-1508 deferred:
| # | Category | What it does | Representative functions |
|---|---|---|---|
| 1 | Frontmatter extraction & reconstruction | Extract (name, description, allowed-tools, argument-hint, agent, context, effort) from the source command frontmatter; reconstruct in the runtime's skill frontmatter shape. |
extractFrontmatterAndBody, skillFrontmatterName, every convertClaudeCommandTo*Skill |
| 2 | Description truncation | Runtimes with description-length limits truncate to the cap (e.g. Codex: 180 chars → metadata.short-description). |
convertClaudeCommandToCodexSkill (toSingleLine + 177-char slice) |
| 3 | Tool-name rewrites | Map Claude tool names to runtime equivalents. | convertToolName, convertKimiToolName, convertCopilotToolName, convertGeminiToolName; inline: AskUserQuestion→question, SlashCommand→skill (opencode) |
| 4 | Path rewrites | ~/.claude → the runtime's config path; computePathPrefix derives the install-target prefix; transformContentToHyphen normalizes /gsd:<cmd> → gsd-<cmd>. |
computePathPrefix, applyOpencodeFamilyPathPrefix, convertClaudeToOpencodeFrontmatter |
| 5 | Slash-command → skill-mention conversion | For runtimes that surface skills (not slash commands), rewrite /gsd:<cmd> invocations into skill-tool mentions. |
convertSlashCommandsTo{Cursor,Windsurf,Augment,Trae,Codebuddy}SkillMentions |
| 6 | Runtime-specific branding / fields | Emit runtime-required frontmatter the source does not carry. | Hermes: version:; Qwen: numeric priority: (QWEN_SKILL_PRIORITY); Codex: metadata.short-description; Kimi: name normalization |
| 7 | Agent-reference neutralization | For non-Claude runtimes, replace "Claude" → "the agent" and CLAUDE.md → the runtime's instruction file. |
neutralizeAgentReferences |
| 8 | Body format (SKILL.md-vs-flat) | Governed by the layout nesting flag + the stage closure: nested runtimes ship <router>/skills/<name>/SKILL.md; flat runtimes ship <prefix><stem>/SKILL.md at one level. |
stageSkillsForRuntimeAsSkills (in install-profiles.cts), buildNamespaceBundleMap |
A converter's contract is the fixed subset of these eight categories it applies, in order. Transform order is load-bearing for byte-parity (cf. ADR-1235 §0): stale-cleanup → path-prefix rewrite → processAttribution → runtime converter/branding → body normalization → filename rename. A converter that silently inherits another's ordering breaks byte-for-byte parity without a test signal.
4. The per-runtime skill mapping
The full 16-runtime matrix — dest path, prefix, nesting, loader recursion, converter, and per-runtime notes — lives in the companion reference page: docs/reference/skill-mapping-matrix.md. The authoritative source for any cell is the runtime's capabilities/<runtime>/capability.json artifactLayout descriptor (resolved by resolveRuntimeArtifactLayout in runtime-artifact-layout.cts); the reference page is the human-readable projection, kept in sync going forward.
Three structural facts the matrix encodes:
- All 15 skill-bearing runtimes use
prefix: "gsd-". (Gemini is commands-only — no skills kind.) - Six runtimes nest under
gsd-ns-*routers (cline, qwen, hermes, augment, trae, antigravity) because their skill loaders scan one level deep; the rest stay flat because their loaders recurse (cursor, opencode, kilo) or because nesting was reverted (claude — Skill-tool errors on unknown names, #924). - Three runtimes share
convertClaudeCommandToClaudeSkill(claude, qwen, hermes); the other 12 skill-bearing runtimes each have a dedicated converter.
5. Plugin / external-skill provision + consumption methodology
GSD's first-party plugin/extension on every supported platform should both provide its own skills and consume external/plugin-provided skills through each platform's documented, native mechanism — never by reaching into an undocumented or ephemeral cache.
Provision — ship GSD's skills the platform-native way:
- Claude Code: the
.claude-plugin/plugin.jsonmanifest should declare askillsfield /skills/dir (today it declares onlycommands+hooks, per ADR-766). This is Phase B-provide / Phase D. - Other platforms: assessed per-platform in Phase C; where a platform has no documented skill-provision model, record N/A with rationale.
Consumption — resolve plugin/external skills through the platform's native skill-resolution mechanism:
- Claude Code: the sub-agent
skills:frontmatter preload (full content injected) and the runtimeSkilltool (loads a namespaced skill by name). PR #1261 (Phase B consume side, merged 2026-06-15) is the reference implementation:agent_skillsaccepts the namespaced formglobal:<plugin>:<skill>and emits a by-name Skill-tool directive — no cache path is ever read. - Other platforms: assessed per-platform in Phase C.
Rejected: reading another plugin's ephemeral cache (e.g. Claude Code's ${CLAUDE_PLUGIN_ROOT} / ~/.claude/plugins/cache, which "changes when the plugin updates"), or copying skill files to undocumented locations. These are workarounds, not fixes — the platform's native mechanism is the contract.
Consequences
- + One authoritative description of the per-runtime skill mapping + converter transform contracts. A future maintainer or agent reads this ADR + the reference matrix instead of reverse-engineering three sources.
- + Unblocks Phases B-provide, C (C1–C6), and D of epic #1258 — each per-platform implementation cites this ADR as its methodology contract.
- + ADR-1016's header reflects reality (Accepted, not Proposed) — the ADR README index is corrected.
- + Closes the documentation leak adjacent to ADR-1508: the module has a home (ADR-1508), the methodology it implements has a record (this ADR).
- − The reference matrix must stay in sync with the capability descriptors. The descriptors (
capabilities/<runtime>/capability.jsonartifactLayout) remain the source of truth; the reference page is a projection. A future runtime addition must update both the descriptor and the matrix row (the descriptor'sTypeErroron unknown runtime is the structural guard; the matrix drift is a documentation gap, not a runtime failure). - − The eight transform-contract categories are descriptive, not type-enforced. A converter that grows a ninth category does not trip a gate — the closed
ConverterNameenum (ADR-1016) gates the set of converters, not the shape of each converter's transform.
Relationship to other ADRs and issues
- ADR-3660 (layout owner, Accepted) — extended, not duplicated. This ADR documents the converter contracts that ADR-3660's
stageclosure binds but does not describe. - ADR-1016 (enum owner, header corrected to Accepted here) — the closed
ConverterNameenum is the type-enforcement substrate; this ADR documents what each named converter does. - ADR-1508 (module owner, Accepted) — sibling. ADR-1508 decides module ownership + dependency direction; this ADR decides methodology + transform contracts. ADR-1508's Phase 1–2 implementation (epic #1507, relocating helpers inside
runtime-artifact-conversion.cts) touches the same file this ADR documents — they are sequenced, not conflicting. - ADR-766 (Claude plugin manifest, Accepted) — referenced for the provision methodology (the
skillsmanifest field Phase B-provide / Phase D adds). - ADR-1235 (descriptor-driven agent conversion) — complementary; its byte-parity transform-ordering rule is cited in Decision 3.
- Epic #1258 — this is Phase A. Phase B-consume (PR #1261, merged) is the reference implementation canonized in Decision 5. Phases B-provide, C (C1–C6), D are tracked as separate issues per the epic's governance.
Amendment — 2026-09-14 (#4727)
Dimension 3's helper roster changed name, not behavior. The table above is left as written
per docs/adr/README.md: "ADRs are append-only. Amendments extend existing ADRs with a dated
section rather than replacing them." Read the row through this amendment:
| Dimension 3 helper, as written above | now named |
|---|---|
convertGeminiToolName |
convertAntigravityToolName |
(its backing table, claudeToGeminiTools) |
claudeToAntigravityTools |
The methodology this ADR decided is unchanged: dimension 3 still maps Claude tool names to
runtime equivalents, through the same helper with the same mapping table and the same exclusion
set. Only the identifier moved, and only because GSD has had no Gemini runtime since #1928
(Google sunset Gemini CLI on 2026-06-18, removal shipped 1.8.0). The sole consumer was, and
remains, convertClaudeAgentToAntigravityAgent.
The mapped values are untouched and must stay so — read_file, write_file, replace,
run_shell_command, glob, search_file_content, google_web_search, web_fetch,
write_todos are Gemini's built-in tool dialect, which Antigravity genuinely speaks. That
dialect is Google's contract; the identifier was GSD's own choice. ConverterName (ADR-1016's
closed enum, and VALID_CONVERTER_NAMES in gsd-core/bin/lib/capability-validator.cjs) is
not affected: it admits only convertClaude{Command,Agent}To* names, and neither renamed
symbol was ever a member, so no capability descriptor's converter field changes.