Skip to content

Conversation

@TabishB
Copy link
Contributor

@TabishB TabishB commented Jan 22, 2026

Summary

  • Add --tool flag to artifact-experimental-setup command to generate skills and commands for different AI tools
  • Create command-generation module with tool-specific adapters for Claude, Cursor, and Windsurf
  • Each adapter handles tool-specific file paths and frontmatter formats
  • Add skillsDir field to AIToolOption interface to map tools to their skill directories

Test plan

  • Run npm run build - passes
  • Run npm run test - all 1064 tests pass
  • Test openspec artifact-experimental-setup --tool claude creates files in .claude/
  • Test openspec artifact-experimental-setup --tool cursor creates files in .cursor/
  • Test openspec artifact-experimental-setup --tool windsurf creates files in .windsurf/
  • Test missing --tool flag shows error with list of valid tools
  • Test unknown tool shows error

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Required --tool flag to target a specific AI tool for skill generation
    • Per-tool skills directory support and multi-provider command generation with provider-specific formatting
    • Adapter-based command generation across many providers and improved CLI messages showing selected tool and paths
  • Documentation

    • Added design and specification docs detailing multi-provider command-generation behavior and error scenarios
  • Tests

    • Extensive unit and integration tests for adapters, registry, generator, and CLI validations

✏️ Tip: You can customize this high-level summary in your review settings.

Add --tool flag to artifact-experimental-setup command to generate
skills and commands for different AI tools (Claude, Cursor, Windsurf).

- Add skillsDir field to AIToolOption interface
- Create command-generation module with tool-specific adapters
- Each adapter handles tool-specific file paths and frontmatter formats
- Add CommandAdapterRegistry for adapter lookup
- Update artifact-experimental-setup to use dynamic paths
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds a multi-provider skill and command generation system: per-tool skillsDir config, a command-adapter strategy (types, adapters, registry, generator), CLI --tool integration for artifact setup, adapter implementations, and tests/specs.

Changes

Cohort / File(s) Summary
Design & Specs
openspec/changes/multi-provider-skill-generation/.openspec.yaml, .../design.md, .../proposal.md, .../specs/*, .../tasks.md
New design/proposal and Delta specs describing skillsDir, adapter strategy, CLI --tool behavior, path rules, and implementation tasks.
Core Config
src/core/config.ts
Adds optional skillsDir?: string to AIToolOption and populates AI_TOOLS entries with per-tool skillsDir values (e.g., .claude, .cursor, .windsurf, etc.).
Command Generation Types
src/core/command-generation/types.ts
New interfaces: CommandContent, ToolCommandAdapter, and GeneratedCommand.
Adapters (impl + barrel)
src/core/command-generation/adapters/* (e.g., claude.ts, cursor.ts, windsurf.ts, amazon-q.ts, antigravity.ts, ..., index.ts)
Many new per-tool adapters implementing toolId, getFilePath, and formatFile with tool-specific frontmatter/path rules; adapters/index.ts re-exports adapters.
Registry & Generator
src/core/command-generation/registry.ts, src/core/command-generation/generator.ts, src/core/command-generation/index.ts
CommandAdapterRegistry (register/get/getAll/has) with built-in registration; generateCommand / generateCommands; barrel exports public API.
CLI Integration
src/commands/artifact-workflow.ts
Refactors artifact-experimental-setup to accept ArtifactExperimentalSetupOptions, adds required --tool flag, validates tool & skillsDir, uses tool.skillsDir and adapter-driven command generation, and updates messages and wiring.
Tests
test/core/command-generation/*.test.ts, test/commands/artifact-workflow.test.ts
New unit/integration tests for types, adapters, generator, registry, and CLI flows (tool validation and generated outputs).

Sequence Diagram(s)

sequenceDiagram
    participant User as User / CLI
    participant CLI as artifact-experimental-setup
    participant Validator as Tool Validator
    participant Registry as CommandAdapterRegistry
    participant Adapter as Tool Adapter
    participant Generator as Command Generator
    participant FS as File System

    User->>CLI: run with --tool <tool-id>
    CLI->>Validator: validate tool exists & has skillsDir
    alt valid tool
        Validator-->>CLI: ok
        CLI->>Registry: get(<tool-id>)
        Registry-->>CLI: adapter (or undefined)
        alt adapter found
            CLI->>Generator: generateCommands(contents, adapter)
            Generator->>Adapter: getFilePath(id)
            Adapter-->>Generator: path (e.g., .claude/...)
            Generator->>Adapter: formatFile(content)
            Adapter-->>Generator: fileContent (frontmatter + body)
            Generator-->>CLI: GeneratedCommand[]
            CLI->>FS: write skill files & command files
            FS-->>CLI: success
            CLI-->>User: success message with tool and paths
        else adapter missing
            Registry-->>CLI: undefined
            CLI-->>User: skip command generation (adapter missing)
        end
    else invalid tool
        Validator-->>CLI: error
        CLI-->>User: error listing valid tool IDs
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • Israel-Laguan

Poem

🐰 I hopped through code with a tiny spring,

adapters blooming like carrots in ring,
Claude, Cursor, Windsurf learned to sing,
registry hums and commands take wing—
nibble the paths, hooray! I bring!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add multi-provider skill generation support' clearly and directly summarizes the main change: adding multi-provider support to the CLI for skill generation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vibe-kanban-cloud
Copy link

Review Complete

Your review story is ready!

View Story

Comment !reviewfast on this PR to re-generate the story.

@greptile-apps
Copy link

greptile-apps bot commented Jan 22, 2026

Greptile Summary

Added multi-provider skill generation support to the artifact-experimental-setup command, enabling skill and command file generation for different AI coding tools (Claude, Cursor, Windsurf).

Key changes:

  • Added --tool flag (required) to artifact-experimental-setup command for explicit tool selection
  • Extended AIToolOption interface with skillsDir field to map tools to their project-local directories
  • Created command-generation module using adapter pattern:
    • CommandContent interface for tool-agnostic command data
    • ToolCommandAdapter interface for tool-specific formatting (file paths, frontmatter)
    • CommandAdapterRegistry for managing adapters with static initialization
    • Tool-specific adapters for Claude (.claude/commands/opsx/), Cursor (.cursor/commands/opsx-*.md), and Windsurf (.windsurf/commands/opsx/)
  • Skills follow Agent Skills spec and are generated to {toolDir}/skills/ for all tools
  • Commands use adapter pattern to handle tool-specific frontmatter formats (Claude/Windsurf use tags array, Cursor uses /opsx- prefix without tags)
  • Comprehensive test coverage (78 new tests covering adapters, generator, registry, and CLI validation)

Design follows OpenSpec guidelines:

  • Proper proposal, design, and spec deltas under openspec/changes/multi-provider-skill-generation/
  • All 1064 tests pass per test plan
  • No breaking changes - requires explicit --tool flag (removed default behavior for clarity)

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Well-architected feature addition with clean adapter pattern, comprehensive test coverage (78 new tests), follows OpenSpec proposal process completely, no breaking changes, and all 1064 tests passing
  • No files require special attention

Important Files Changed

Filename Overview
src/commands/artifact-workflow.ts Added --tool flag to artifact-experimental-setup command with validation and multi-provider support using adapter pattern
src/core/config.ts Extended AIToolOption interface with skillsDir field to map tools to their skill directories
src/core/command-generation/types.ts Defines tool-agnostic command interfaces (CommandContent, ToolCommandAdapter) for multi-provider generation
src/core/command-generation/registry.ts Implements adapter registry pattern with static initialization for Claude, Cursor, and Windsurf adapters
src/core/command-generation/generator.ts Provides command generation functions using adapter pattern to format tool-specific output
src/core/command-generation/adapters/claude.ts Implements Claude Code adapter with frontmatter format (name, description, category, tags array)
src/core/command-generation/adapters/cursor.ts Implements Cursor adapter with /opsx- name prefix and no tags field in frontmatter
src/core/command-generation/adapters/windsurf.ts Implements Windsurf adapter with same frontmatter format as Claude

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as artifact-experimental-setup
    participant Config as AI_TOOLS config
    participant Registry as CommandAdapterRegistry
    participant Adapter as ToolCommandAdapter
    participant FS as FileSystem

    User->>CLI: openspec artifact-experimental-setup --tool cursor
    CLI->>CLI: Validate --tool flag provided
    CLI->>Config: Find tool in AI_TOOLS
    Config-->>CLI: Return tool config with skillsDir
    CLI->>CLI: Validate tool has skillsDir
    
    CLI->>FS: Create skills in {skillsDir}/skills/
    Note over FS: e.g., .cursor/skills/openspec-explore/SKILL.md
    
    CLI->>Registry: Get adapter for tool
    Registry-->>CLI: Return ToolCommandAdapter
    
    CLI->>CLI: Convert templates to CommandContent[]
    CLI->>Adapter: generateCommands(contents, adapter)
    
    loop For each command
        Adapter->>Adapter: getFilePath(commandId)
        Note over Adapter: Tool-specific path logic
        Adapter->>Adapter: formatFile(content)
        Note over Adapter: Tool-specific frontmatter
        Adapter-->>CLI: Return GeneratedCommand
    end
    
    CLI->>FS: Write command files
    Note over FS: e.g., .cursor/commands/opsx-explore.md
    
    CLI-->>User: Success with file list
Loading

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@openspec/changes/multi-provider-skill-generation/design.md`:
- Around line 50-67: The fenced ASCII diagram block containing "CommandContent"
and the "generateCommand(content, adapter)" flow is missing a language
specifier; update that code fence to include a plain text language (e.g., add
"text" or "plaintext" after the opening ```), so the markdown linter recognizes
it while leaving the diagram content (including identifiers like CommandContent,
generateCommand(content, adapter), and the Claude/Cursor/Windsurf Adapter
labels) unchanged.

In `@openspec/changes/multi-provider-skill-generation/proposal.md`:
- Around line 28-34: Update the proposal to include explicit line-numbered file
references for each referenced file entry; specifically, add file:line style
references next to the entries that mention AIToolOption, the artifact-workflow
--tool flag, CommandContent, ToolCommandAdapter, the generic command generator,
and the per-tool adapters so reviewers can jump directly to the exact lines in
the implementation; ensure each list item shows the target file and a plausible
example line number for the declaration of AIToolOption, the --tool flag
handling, the CommandContent/ToolCommandAdapter interfaces, the generator
implementation, and adapter files.
- Around line 8-14: The docs claim the `--tool <tool-id>` flag is optional and
defaults to `claude`, but the implementation requires the flag; update the
proposal to mark this as BREAKING by stating the `--tool` flag is now required
for the `artifact-experimental-setup` command (remove or correct the
"default-to-claude" wording), and add a clear BREAKING change note in the
proposal sections that currently reference default/optional behavior
(references: `artifact-experimental-setup`, `--tool`, and the description of
default-to-claude in the CommandGenerator/adapter paragraphs) so readers know
they must pass `--tool`.

In
`@openspec/changes/multi-provider-skill-generation/specs/ai-tool-paths/spec.md`:
- Around line 15-18: Replace the permissive wording "MAY include a `skillsDir`
field" with normative wording ("SHALL include a `skillsDir` field") in the
scenario that describes tool entries in AI_TOOLS so the spec requires a
project-local base directory; update the sentence under "Scenario: Interface
includes skillsDir field" to state that a tool entry in AI_TOOLS SHALL include a
`skillsDir` field specifying the project-local base directory (e.g., `.claude`).

In
`@openspec/changes/multi-provider-skill-generation/specs/command-generation/spec.md`:
- Around line 7-10: Remove the extra top-level "## Requirements" header so the
delta spec uses only the delta-style header ("## ADDED Requirements" / "##
MODIFIED Requirements" / "## REMOVED Requirements" / "## RENAMED Requirements");
specifically delete the standalone "## Requirements" line introduced before the
"## ADDED Requirements" header in spec.md so the file conforms to the required
delta spec header format.
- Around line 38-49: Add a new "Scenario: Windsurf adapter formatting" to the
command-generation spec that mirrors the other adapter scenarios: WHEN
formatting a command for the Windsurf adapter, THEN the adapter SHALL output
YAML frontmatter containing the fields name (formatted as opsx-<id>), id,
category, description and tags, AND the file path SHALL follow the pattern
.windsurf/commands/opsx-<id>.md so the Windsurf behavior is normative and
testable; place this scenario alongside the existing Claude/Cursor scenarios in
spec.md using the exact scenario header "Scenario: Windsurf adapter formatting".

In `@src/commands/artifact-workflow.ts`:
- Around line 848-854: The custom missing --tool error is never reached because
the CLI uses Commander.js .requiredOption(), which causes Commander to exit
before the action handler runs; change the flag declaration to use .option()
instead so the action handler executes and the existing validation (checking
options.tool and calling getToolsWithSkillsDir()) can run and throw the custom
Error with the list of valid tools. Update the command setup where
.requiredOption('--tool', ...) is used to .option('--tool', ...), leaving the
action handler that references options.tool and getToolsWithSkillsDir() intact.

In `@src/core/command-generation/adapters/cursor.ts`:
- Around line 23-33: The frontmatter built by formatFile in cursor.ts uses raw
interpolation of CommandContent fields (category, description) which can break
YAML if they contain colons, newlines, or other special chars; update formatFile
to safely quote or escape these values (e.g., wrap category and id fields in
double quotes and render description using a YAML block scalar or properly
escaped/quoted string) so the generated frontmatter is always valid even when
content.category or content.description contain special characters.

In `@src/core/command-generation/adapters/windsurf.ts`:
- Around line 23-33: The YAML frontmatter produced by formatFile in
adapters/windsurf.ts is vulnerable to breaking when content fields or tags
contain commas, colons, or quotes; update formatFile (function formatFile, type
CommandContent) to quote scalar fields (name, description, category) and
serialize tags as a YAML/JSON-safe sequence instead of interpolating tagsStr
(e.g., emit tags: ["tag1","tag,with,comma"] using a JSON.stringify or a
YAML-safe serializer) and ensure body remains separated; this prevents unescaped
characters from corrupting the frontmatter.

In `@test/commands/artifact-workflow.test.ts`:
- Around line 607-650: The assertions checking CLI output for platform-specific
paths are flaky; update the three tests that call getOutput() (the 'creates
skills for Claude tool', 'Cursor', and 'Windsurf' it blocks) to normalize
separators before asserting — e.g., compute const normalized =
getOutput(result).replace(/\\+/g, '/') (or use path.posix) and then assert
normalized.includes('.claude/skills/'), '.cursor/skills/' and
'.windsurf/skills/' respectively; also use the same normalization when checking
any other CLI path substrings in these tests (references: runCLI, getOutput,
tempDir, and the three test case descriptions).
🧹 Nitpick comments (1)
test/core/command-generation/adapters.test.ts (1)

85-106: Consider adding empty tags test for windsurfAdapter for consistency.

The claudeAdapter tests include an edge case for empty tags (lines 45-49), but windsurfAdapter shares the same formatting logic and doesn't have this test. Since both adapters use content.tags.join(', '), the behavior should be consistent, but adding the test would ensure parity.

Suggested addition
    it('should handle empty tags', () => {
      const contentNoTags: CommandContent = { ...sampleContent, tags: [] };
      const output = windsurfAdapter.formatFile(contentNoTags);
      expect(output).toContain('tags: []');
    });

Comment on lines +28 to +34
- **Files Modified**:
- `src/core/config.ts` - Extend `AIToolOption` interface with `skillsDir` field
- `src/commands/artifact-workflow.ts` - Add `--tool` flag, use provider paths and adapters
- **New Files**:
- `src/core/command-generation/types.ts` - CommandContent, ToolCommandAdapter interfaces
- `src/core/command-generation/generator.ts` - Generic command generator
- `src/core/command-generation/adapters/*.ts` - Per-tool adapters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add line-numbered file references.

The proposal references files without line numbers; please use direct references like src/core/config.ts:19 to meet the documentation format requirement.

As per coding guidelines, use file.ts:line references in proposal/design/tasks.

🤖 Prompt for AI Agents
In `@openspec/changes/multi-provider-skill-generation/proposal.md` around lines 28
- 34, Update the proposal to include explicit line-numbered file references for
each referenced file entry; specifically, add file:line style references next to
the entries that mention AIToolOption, the artifact-workflow --tool flag,
CommandContent, ToolCommandAdapter, the generic command generator, and the
per-tool adapters so reviewers can jump directly to the exact lines in the
implementation; ensure each list item shows the target file and a plausible
example line number for the declaration of AIToolOption, the --tool flag
handling, the CommandContent/ToolCommandAdapter interfaces, the generator
implementation, and adapter files.

Add skillsDir mappings for tools that were missing:
- Amazon Q Developer (.amazonq)
- Antigravity (.agent)
- Auggie (.augment)
- Cline (.cline)
- CodeBuddy Code (.codebuddy)
- Continue (.continue)
- CoStrict (.cospec)
- Crush (.crush)
- iFlow (.iflow)
- Qoder (.qoder)
- Qwen Code (.qwen)

Fix RooCode path: .roocode → .roo
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/core/config.ts`:
- Around line 21-41: Update the incorrect skillsDir values for the three tools
so generation writes to the tool-specific subdirectories: change the Amazon Q
Developer entry (value 'amazon-q') skillsDir from '.amazonq' to
'.amazonq/rules', change the Auggie (Augment CLI) entry (value 'auggie')
skillsDir from '.augment' to '.augment/commands', and change the Cursor entry
(value 'cursor') skillsDir from '.cursor' to '.cursor/agents'; locate and modify
these objects in the array in config.ts (refer to the name or value fields to
find each entry).
🧹 Nitpick comments (1)
src/core/config.ts (1)

12-18: Make skillsDir required for available tools to avoid runtime gaps.
Line 17: Since adapters rely on skillsDir, keeping it optional allows available: true entries without a directory. Consider a discriminated union to enforce this at the type level.

💡 Suggested type tightening
-export interface AIToolOption {
-  name: string;
-  value: string;
-  available: boolean;
-  successLabel?: string;
-  skillsDir?: string; // e.g., '.claude' - /skills suffix per Agent Skills spec
-}
+export type AIToolOption =
+  | {
+      name: string;
+      value: string;
+      available: true;
+      successLabel?: string;
+      skillsDir: string; // e.g., '.claude' - /skills suffix per Agent Skills spec
+    }
+  | {
+      name: string;
+      value: string;
+      available: false;
+      successLabel?: string;
+      skillsDir?: undefined;
+    };

Add 18 new command adapters covering all supported AI tools in the
multi-provider skill generation system. Each adapter implements the
correct file path and frontmatter format for its respective tool.

New adapters: amazon-q, antigravity, auggie, cline, codex, codebuddy,
continue, costrict, crush, factory, gemini, github-copilot, iflow,
kilocode, opencode, qoder, qwen, roocode.
- Change .requiredOption to .option for custom error handling with tool list
- Add YAML escaping for special characters in all command adapters
- Normalize path separators in tests for cross-platform compatibility
- Update docs: --tool flag is required, not optional with default
- Add missing Windsurf adapter scenario to spec
- Fix spec headers and language specifiers
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@openspec/changes/multi-provider-skill-generation/design.md`:
- Around line 3-5: Update the design doc to use explicit file:line references
for the code artifacts instead of generic names: replace mentions of config.ts,
AI_TOOLS, SlashCommandConfigurator, and artifact-experimental-setup with the
precise file.ts:line locations that host those symbols in the repo (pointing to
the actual source file and line for the AI_TOOLS array, the config.ts module,
the SlashCommandConfigurator class, and the artifact-experimental-setup command
implementation), ensuring each reference follows the file.ts:line format used by
the codebase and keeping the symbol names (AI_TOOLS, SlashCommandConfigurator,
artifact-experimental-setup) so readers can correlate doc text with code.

In `@src/core/command-generation/adapters/antigravity.ts`:
- Around line 22-29: The YAML frontmatter in formatFile is inserting
content.description raw, which can break YAML when it contains special chars;
update formatFile in antigravity.ts to escape/serialize content.description
before interpolation (e.g., call an existing escapeYaml/escapeString helper or
use a small helper that quotes or uses yaml.safeDump/yaml.stringify) and then
insert the escaped value instead of content.description so the generated
frontmatter is always valid.

In `@src/core/command-generation/adapters/codebuddy.ts`:
- Around line 22-31: The frontmatter generation in formatFile (in codebuddy.ts)
blindly injects content.name and content.description causing malformed YAML when
they contain quotes or special chars; update formatFile to call the existing
escapeYamlValue helper for both the name and description fields (e.g., use
escapeYamlValue(content.name) and escapeYamlValue(content.description)) and keep
description quoted as before if the helper expects/returns a safe string,
ensuring the body remains unchanged; this fixes YAML-escaping without altering
CommandContent or other callers.

In `@src/core/command-generation/adapters/codex.ts`:
- Around line 22-30: The formatFile method renders the YAML frontmatter without
escaping the description, which can break the frontmatter if description
contains YAML-special characters; update formatFile (in the codex adapter) to
call escapeYamlValue(content.description) when interpolating the description
into the template (and import/ensure escapeYamlValue is available) so the
description is safely serialized into the frontmatter.

In `@src/core/command-generation/adapters/factory.ts`:
- Around line 22-26: The YAML frontmatter produced by formatFile(CommandContent)
writes content.description unescaped which can break YAML when it contains
colons, newlines or quotes; update formatFile to properly escape or serialize
description (e.g., use a YAML serializer like js-yaml/YAML.stringify or emit a
block scalar) instead of interpolating raw text so the description is always
valid YAML; locate formatFile in the factory adapter and replace the raw
template interpolation with a safe-serialized value for content.description (and
apply same treatment to any other frontmatter fields).

In `@src/core/command-generation/adapters/gemini.ts`:
- Around line 22-29: The formatFile method in adapters/gemini.ts writes raw
content.description and content.body into TOML, which breaks if those strings
contain quotes or triple quotes; update formatFile to escape both fields using
the project's existing escapeYamlValue helper (or add an escapeTomlValue if more
appropriate) and use the escaped values when composing the TOML string so
description and prompt are TOML-safe; specifically, modify formatFile to call
escapeYamlValue(content.description) and escapeYamlValue(content.body) (or the
new escape function) and interpolate those escaped results instead of the raw
content.

In `@src/core/command-generation/adapters/iflow.ts`:
- Around line 22-32: The formatFile method inserts content.category and
content.description directly into YAML frontmatter which can break when they
contain special YAML characters; update formatFile (in
src/core/command-generation/adapters/iflow.ts) to properly escape or serialize
those fields before embedding them (e.g., use a YAML serializer or safe-escape
function for category and description, or quote and escape newline/colon
characters) so the generated frontmatter remains valid for any content values.

In `@src/core/command-generation/adapters/opencode.ts`:
- Around line 22-29: The YAML frontmatter in formatFile (in opencode.ts) inserts
content.description without escaping, which breaks YAML when description
contains special chars; update formatFile to pass content.description through
the same YAML-escaping helper used elsewhere (e.g., import and call
escapeYamlValue or implement a local escapeYamlValue) so the description is
safely escaped before embedding in the frontmatter, preserving the existing
template and returning the escaped string.

In `@src/core/command-generation/adapters/qoder.ts`:
- Around line 22-33: formatFile currently interpolates CommandContent.name,
description, category and tags directly into YAML which can break when values
contain special characters; update formatFile (in
src/core/command-generation/adapters/qoder.ts) to use the same helpers as
claude/windsurf: call escapeYamlValue() for name, description and category and
use formatTagsArray() (or escapeYamlValue per-tag) to produce a safely escaped
tags array string, ensuring double-quoting and backslash-escaping of
quotes/newlines as those helpers do; preserve the same frontmatter layout but
replace raw interpolations with the escaped values and ensure tagsStr is built
from escaped tags.

In `@src/core/command-generation/adapters/qwen.ts`:
- Around line 22-28: formatFile inserts content.description directly into a TOML
basic string which can produce invalid TOML if it contains quotes, backslashes,
or newlines; update formatFile (in the qwen adapter) to run content.description
through a TOML-escaping helper (e.g., escapeTomlBasicString or similar) that
escapes backslashes and double quotes and encodes control/newline characters
before interpolation, then use the escaped string in the description line;
reference the formatFile method and the CommandContent.description field when
making the change.
♻️ Duplicate comments (3)
openspec/changes/multi-provider-skill-generation/proposal.md (3)

7-13: Mark required --tool as BREAKING and align backward-compat text.
The command now requires --tool, so the “Backward Compatibility” note is misleading unless updated.

As per coding guidelines, include BREAKING markers where applicable.

✏️ Suggested update
- - Add required `--tool <tool-id>` flag to the `artifact-experimental-setup` command
+ - **BREAKING** Require `--tool <tool-id>` for `artifact-experimental-setup`

- - **Backward Compatibility**: Existing workflows unaffected - this is a new command setup feature
+ - **Backward Compatibility**: **BREAKING** — `artifact-experimental-setup` now requires `--tool`

Also applies to: 35-36


28-34: Add file:line references to modified/new file lists.
The Impact section should use direct references like src/core/config.ts:42.

As per coding guidelines, use file.ts:line references in proposal/design/tasks.


1-3: Mark the required --tool flag with **BREAKING** and clarify backward compatibility.

The proposal structure follows AGENTS.md guidelines but has two content issues:

  1. The required --tool flag is a breaking change and should be marked with **BREAKING** in the "What Changes" section (currently unmarked).

  2. The "Impact" section contradicts itself: it claims "Existing workflows unaffected" but also states the flag is "Required". If the flag is mandatory, existing artifact-experimental-setup invocations without --tool will break. Revise to accurately reflect that this command now requires the flag.

File references (e.g., src/core/config.ts) could include line numbers for precision, though this is optional.

🧹 Nitpick comments (6)
src/core/command-generation/adapters/claude.ts (1)

14-31: Consider extracting YAML escaping helpers to a shared utility.

The escapeYamlValue and formatTagsArray functions are well-implemented. However, other adapters (e.g., opencode.ts, antigravity.ts, iflow.ts) also need YAML escaping but currently lack it. Extracting these helpers to a shared module (e.g., src/core/command-generation/utils/yaml.ts) would:

  1. Ensure consistent escaping across all adapters
  2. Reduce code duplication
  3. Make it easier to maintain and test the escaping logic
test/commands/artifact-workflow.test.ts (1)

614-662: Consider adding command file verification for Claude and Windsurf tests.

The Cursor test (lines 643-646) verifies both SKILL.md creation and command file content, but Claude and Windsurf tests only verify SKILL.md. For consistency and better coverage, consider adding similar command file assertions.

💡 Example additions

For Claude test (after line 626):

// Verify commands were created with Claude format
const commandFile = path.join(tempDir, '.claude', 'commands', 'opsx', 'explore.md');
const content = await fs.readFile(commandFile, 'utf-8');
expect(content).toContain('name:');
expect(content).toContain('tags:');

For Windsurf test (after line 661):

// Verify commands were created with Windsurf format
const commandFile = path.join(tempDir, '.windsurf', 'commands', 'opsx-explore.md');
const content = await fs.readFile(commandFile, 'utf-8');
// Add appropriate format assertions
test/core/command-generation/registry.test.ts (1)

35-49: Consider using exact count or documenting adapter expectations.

The test uses toBeGreaterThanOrEqual(3) but the registry snippet shows 21 adapters are registered. While the minimum check works, it could mask regressions if adapters are accidentally removed.

♻️ Optional: Use snapshot or exact count
   describe('getAll', () => {
     it('should return array of all registered adapters', () => {
       const adapters = CommandAdapterRegistry.getAll();
       expect(Array.isArray(adapters)).toBe(true);
-      expect(adapters.length).toBeGreaterThanOrEqual(3); // At least Claude, Cursor, Windsurf
+      expect(adapters.length).toBe(21); // All registered adapters
     });

Alternatively, use toMatchInlineSnapshot() on the toolIds array to catch additions/removals.

openspec/changes/multi-provider-skill-generation/specs/ai-tool-paths/spec.md (1)

60-63: Consider adding parallel structure to Unix scenario.

The Windows scenario (lines 54-58) includes an **AND** SHALL NOT hardcode forward slashes clause, but the Unix scenario lacks a corresponding constraint. For consistency and completeness, consider adding a similar clause.

✏️ Suggested addition for consistency
 #### Scenario: Path construction on Unix

 - **WHEN** constructing skill paths on macOS or Linux
 - **THEN** the system SHALL use `path.join()` for consistency
+- **AND** SHALL NOT hardcode path separators
src/core/command-generation/registry.ts (1)

66-68: Prevent silent adapter overrides on duplicate tool IDs.
Right now register will overwrite an existing adapter silently, which can hide accidental duplicate tool IDs. Consider guarding and throwing (or logging) on duplicates to fail fast.

♻️ Suggested guard
   static register(adapter: ToolCommandAdapter): void {
-    CommandAdapterRegistry.adapters.set(adapter.toolId, adapter);
+    if (CommandAdapterRegistry.adapters.has(adapter.toolId)) {
+      throw new Error(`Command adapter already registered: ${adapter.toolId}`);
+    }
+    CommandAdapterRegistry.adapters.set(adapter.toolId, adapter);
   }
src/core/command-generation/adapters/cursor.ts (1)

38-44: Consider escaping name and id for full YAML safety.
If command IDs ever include special characters, name/id can break YAML; using the existing escape helper keeps all fields consistent.

♻️ Suggested change
   formatFile(content: CommandContent): string {
     return `---
-name: /opsx-${content.id}
-id: opsx-${content.id}
+name: ${escapeYamlValue(`/opsx-${content.id}`)}
+id: ${escapeYamlValue(`opsx-${content.id}`)}
 category: ${escapeYamlValue(content.category)}
 description: ${escapeYamlValue(content.description)}
 ---

Comment on lines +3 to +5
The `artifact-experimental-setup` command generates skill files and opsx slash commands for AI coding assistants. Currently it hardcodes paths to `.claude/skills` and `.claude/commands/opsx`.

The existing `AI_TOOLS` array in `config.ts` lists 22 AI tools but lacks path information. There's also an existing `SlashCommandConfigurator` system for the old workflow commands, but it's tightly coupled to the old 3 commands (proposal, apply, archive) and can't be easily extended for the 9 opsx commands.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use direct file references for mentioned code artifacts.
Please update references like config.ts and SlashCommandConfigurator to the file.ts:line format (e.g., src/core/config.ts:42) for clarity. As per coding guidelines, use direct file references in these design docs.

🤖 Prompt for AI Agents
In `@openspec/changes/multi-provider-skill-generation/design.md` around lines 3 -
5, Update the design doc to use explicit file:line references for the code
artifacts instead of generic names: replace mentions of config.ts, AI_TOOLS,
SlashCommandConfigurator, and artifact-experimental-setup with the precise
file.ts:line locations that host those symbols in the repo (pointing to the
actual source file and line for the AI_TOOLS array, the config.ts module, the
SlashCommandConfigurator class, and the artifact-experimental-setup command
implementation), ensuring each reference follows the file.ts:line format used by
the codebase and keeping the symbol names (AI_TOOLS, SlashCommandConfigurator,
artifact-experimental-setup) so readers can correlate doc text with code.

Comment on lines +22 to +29
formatFile(content: CommandContent): string {
return `---
description: ${content.description}
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing YAML escaping for description field.

Same issue as opencode.ts — the description value should be escaped before being placed in YAML frontmatter to prevent parsing errors from special characters.

🔧 Proposed fix
+function escapeYamlValue(value: string): string {
+  const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
+  if (needsQuoting) {
+    const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
+    return `"${escaped}"`;
+  }
+  return value;
+}
+
 export const antigravityAdapter: ToolCommandAdapter = {
   // ...
   formatFile(content: CommandContent): string {
     return `---
-description: ${content.description}
+description: ${escapeYamlValue(content.description)}
 ---

 ${content.body}
 `;
   },
 };
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/antigravity.ts` around lines 22 - 29,
The YAML frontmatter in formatFile is inserting content.description raw, which
can break YAML when it contains special chars; update formatFile in
antigravity.ts to escape/serialize content.description before interpolation
(e.g., call an existing escapeYaml/escapeString helper or use a small helper
that quotes or uses yaml.safeDump/yaml.stringify) and then insert the escaped
value instead of content.description so the generated frontmatter is always
valid.

Comment on lines +22 to +31
formatFile(content: CommandContent): string {
return `---
name: ${content.name}
description: "${content.description}"
argument-hint: "[command arguments]"
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing YAML escaping for name and description fields.

The name field is unquoted and description uses literal quotes without escaping inner content. If either contains YAML special characters or quotes, the frontmatter will be malformed.

🐛 Proposed fix: Use escapeYamlValue helper
 import path from 'path';
 import type { CommandContent, ToolCommandAdapter } from '../types.js';
+import { escapeYamlValue } from './claude.js'; // or shared utils

   formatFile(content: CommandContent): string {
     return `---
-name: ${content.name}
-description: "${content.description}"
+name: ${escapeYamlValue(content.name)}
+description: ${escapeYamlValue(content.description)}
 argument-hint: "[command arguments]"
 ---

 ${content.body}
 `;
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
formatFile(content: CommandContent): string {
return `---
name: ${content.name}
description: "${content.description}"
argument-hint: "[command arguments]"
---
${content.body}
`;
},
formatFile(content: CommandContent): string {
return `---
name: ${escapeYamlValue(content.name)}
description: ${escapeYamlValue(content.description)}
argument-hint: "[command arguments]"
---
${content.body}
`;
},
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/codebuddy.ts` around lines 22 - 31, The
frontmatter generation in formatFile (in codebuddy.ts) blindly injects
content.name and content.description causing malformed YAML when they contain
quotes or special chars; update formatFile to call the existing escapeYamlValue
helper for both the name and description fields (e.g., use
escapeYamlValue(content.name) and escapeYamlValue(content.description)) and keep
description quoted as before if the helper expects/returns a safe string,
ensuring the body remains unchanged; this fixes YAML-escaping without altering
CommandContent or other callers.

Comment on lines +22 to +30
formatFile(content: CommandContent): string {
return `---
description: ${content.description}
argument-hint: command arguments
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing YAML escaping for description field.

The description field is rendered without escaping. If it contains YAML special characters (:, #, newlines, quotes), the frontmatter will be malformed. Other adapters use escapeYamlValue() for safe output.

🐛 Proposed fix: Use escapeYamlValue helper
 import path from 'path';
 import type { CommandContent, ToolCommandAdapter } from '../types.js';
+import { escapeYamlValue } from './claude.js'; // or wherever the helper is defined

 export const codexAdapter: ToolCommandAdapter = {
   toolId: 'codex',

   getFilePath(commandId: string): string {
     return path.join('.codex', 'prompts', `opsx-${commandId}.md`);
   },

   formatFile(content: CommandContent): string {
     return `---
-description: ${content.description}
+description: ${escapeYamlValue(content.description)}
 argument-hint: command arguments
 ---

 ${content.body}
 `;
   },
 };
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/codex.ts` around lines 22 - 30, The
formatFile method renders the YAML frontmatter without escaping the description,
which can break the frontmatter if description contains YAML-special characters;
update formatFile (in the codex adapter) to call
escapeYamlValue(content.description) when interpolating the description into the
template (and import/ensure escapeYamlValue is available) so the description is
safely serialized into the frontmatter.

Comment on lines +22 to +26
formatFile(content: CommandContent): string {
return `---
description: ${content.description}
argument-hint: command arguments
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape YAML frontmatter values to prevent invalid output.
description is unescaped; colons/newlines/quotes can break YAML.

🐛 Suggested fix
+function escapeYamlValue(value: string): string {
+  const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
+  if (needsQuoting) {
+    const escaped = value
+      .replace(/\\/g, '\\\\')
+      .replace(/"/g, '\\"')
+      .replace(/\n/g, '\\n');
+    return `"${escaped}"`;
+  }
+  return value;
+}
+
 export const factoryAdapter: ToolCommandAdapter = {
   toolId: 'factory',
@@
   formatFile(content: CommandContent): string {
     return `---
-description: ${content.description}
+description: ${escapeYamlValue(content.description)}
 argument-hint: command arguments
 ---
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
formatFile(content: CommandContent): string {
return `---
description: ${content.description}
argument-hint: command arguments
---
function escapeYamlValue(value: string): string {
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
const escaped = value
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}
formatFile(content: CommandContent): string {
return `---
description: ${escapeYamlValue(content.description)}
argument-hint: command arguments
---
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/factory.ts` around lines 22 - 26, The
YAML frontmatter produced by formatFile(CommandContent) writes
content.description unescaped which can break YAML when it contains colons,
newlines or quotes; update formatFile to properly escape or serialize
description (e.g., use a YAML serializer like js-yaml/YAML.stringify or emit a
block scalar) instead of interpolating raw text so the description is always
valid YAML; locate formatFile in the factory adapter and replace the raw
template interpolation with a safe-serialized value for content.description (and
apply same treatment to any other frontmatter fields).

Comment on lines +22 to +29
formatFile(content: CommandContent): string {
return `description = "${content.description}"
prompt = """
${content.body}
"""
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing TOML escaping for description and body fields.

If content.description contains double quotes or content.body contains """, the TOML output will be malformed or could cause unexpected parsing. Other adapters (e.g., claude.ts, cursor.ts) use escapeYamlValue() for safe output.

🐛 Proposed fix: Add TOML-safe escaping
+/**
+ * Escape a string for use in TOML double-quoted context.
+ */
+function escapeTomlString(value: string): string {
+  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+}
+
 export const geminiAdapter: ToolCommandAdapter = {
   toolId: 'gemini',

   getFilePath(commandId: string): string {
     return path.join('.gemini', 'commands', 'opsx', `${commandId}.toml`);
   },

   formatFile(content: CommandContent): string {
-    return `description = "${content.description}"
+    return `description = "${escapeTomlString(content.description)}"

 prompt = """
-${content.body}
+${content.body.replace(/"""/g, "'''")}
 """
 `;
   },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
formatFile(content: CommandContent): string {
return `description = "${content.description}"
prompt = """
${content.body}
"""
`;
},
/**
* Escape a string for use in TOML double-quoted context.
*/
function escapeTomlString(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
export const geminiAdapter: ToolCommandAdapter = {
toolId: 'gemini',
getFilePath(commandId: string): string {
return path.join('.gemini', 'commands', 'opsx', `${commandId}.toml`);
},
formatFile(content: CommandContent): string {
return `description = "${escapeTomlString(content.description)}"
prompt = """
${content.body.replace(/"""/g, "'''")}
"""
`;
},
};
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/gemini.ts` around lines 22 - 29, The
formatFile method in adapters/gemini.ts writes raw content.description and
content.body into TOML, which breaks if those strings contain quotes or triple
quotes; update formatFile to escape both fields using the project's existing
escapeYamlValue helper (or add an escapeTomlValue if more appropriate) and use
the escaped values when composing the TOML string so description and prompt are
TOML-safe; specifically, modify formatFile to call
escapeYamlValue(content.description) and escapeYamlValue(content.body) (or the
new escape function) and interpolate those escaped results instead of the raw
content.

Comment on lines +22 to +32
formatFile(content: CommandContent): string {
return `---
name: /opsx-${content.id}
id: opsx-${content.id}
category: ${content.category}
description: ${content.description}
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing YAML escaping for category and description fields.

The category and description values are inserted directly into YAML frontmatter without escaping. If these contain special YAML characters, the generated file will have invalid frontmatter.

🔧 Proposed fix
+function escapeYamlValue(value: string): string {
+  const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
+  if (needsQuoting) {
+    const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
+    return `"${escaped}"`;
+  }
+  return value;
+}
+
 formatFile(content: CommandContent): string {
   return `---
 name: /opsx-${content.id}
 id: opsx-${content.id}
-category: ${content.category}
-description: ${content.description}
+category: ${escapeYamlValue(content.category)}
+description: ${escapeYamlValue(content.description)}
 ---

 ${content.body}
 `;
 },
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/iflow.ts` around lines 22 - 32, The
formatFile method inserts content.category and content.description directly into
YAML frontmatter which can break when they contain special YAML characters;
update formatFile (in src/core/command-generation/adapters/iflow.ts) to properly
escape or serialize those fields before embedding them (e.g., use a YAML
serializer or safe-escape function for category and description, or quote and
escape newline/colon characters) so the generated frontmatter remains valid for
any content values.

Comment on lines +22 to +29
formatFile(content: CommandContent): string {
return `---
description: ${content.description}
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing YAML escaping for description field.

The description value is inserted directly into YAML frontmatter without escaping. If the description contains special YAML characters (:, #, ", newlines, etc.), this will produce invalid YAML. The claude.ts adapter properly escapes values using escapeYamlValue().

🔧 Proposed fix

Either import and reuse the escape helper from a shared location, or add it locally:

 import path from 'path';
 import type { CommandContent, ToolCommandAdapter } from '../types.js';

+/**
+ * Escapes a string value for safe YAML output.
+ */
+function escapeYamlValue(value: string): string {
+  const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
+  if (needsQuoting) {
+    const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
+    return `"${escaped}"`;
+  }
+  return value;
+}
+
 export const opencodeAdapter: ToolCommandAdapter = {
   toolId: 'opencode',

   getFilePath(commandId: string): string {
     return path.join('.opencode', 'command', `opsx-${commandId}.md`);
   },

   formatFile(content: CommandContent): string {
     return `---
-description: ${content.description}
+description: ${escapeYamlValue(content.description)}
 ---

 ${content.body}
 `;
   },
 };
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/opencode.ts` around lines 22 - 29, The
YAML frontmatter in formatFile (in opencode.ts) inserts content.description
without escaping, which breaks YAML when description contains special chars;
update formatFile to pass content.description through the same YAML-escaping
helper used elsewhere (e.g., import and call escapeYamlValue or implement a
local escapeYamlValue) so the description is safely escaped before embedding in
the frontmatter, preserving the existing template and returning the escaped
string.

Comment on lines +22 to +33
formatFile(content: CommandContent): string {
const tagsStr = content.tags.join(', ');
return `---
name: ${content.name}
description: ${content.description}
category: ${content.category}
tags: [${tagsStr}]
---
${content.body}
`;
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the qoder.ts file to see the actual implementation
cat -n src/core/command-generation/adapters/qoder.ts

Repository: Fission-AI/OpenSpec

Length of output: 1085


🏁 Script executed:

# Search for YAML escaping or sanitization patterns in the command-generation module
rg -n -C3 'escape|yaml|sanitize' --type=ts src/core/command-generation/

Repository: Fission-AI/OpenSpec

Length of output: 7048


🏁 Script executed:

# Check for CommandContent type definition
rg -n 'interface CommandContent|type CommandContent' --type=ts src/core/

Repository: Fission-AI/OpenSpec

Length of output: 289


🏁 Script executed:

# Look at other adapter implementations for comparison
fd -e ts 'adapter' src/core/command-generation/adapters/ | head -10

Repository: Fission-AI/OpenSpec

Length of output: 45


🏁 Script executed:

# Check if there are other adapters to understand the complete pattern
fd -e ts . src/core/command-generation/adapters/

Repository: Fission-AI/OpenSpec

Length of output: 1118


🏁 Script executed:

# Verify the complete adapter pattern by listing all adapters
ls -la src/core/command-generation/adapters/

Repository: Fission-AI/OpenSpec

Length of output: 1494


🏁 Script executed:

# Check a few simpler adapters to see the pattern
cat -n src/core/command-generation/adapters/amazon-q.ts

Repository: Fission-AI/OpenSpec

Length of output: 952


🏁 Script executed:

# Check another one
cat -n src/core/command-generation/adapters/antigravity.ts

Repository: Fission-AI/OpenSpec

Length of output: 948


🏁 Script executed:

# Check gemini to see the pattern
cat -n src/core/command-generation/adapters/gemini.ts

Repository: Fission-AI/OpenSpec

Length of output: 966


Add YAML escaping for frontmatter values to prevent malformed output.

The formatFile method interpolates name, description, category, and tags directly into YAML without escaping. If these values contain YAML special characters (:, #, ", ', newlines, brackets, etc.), the generated file will have malformed YAML.

The claude and windsurf adapters already implement this pattern: use an escapeYamlValue() function to detect special characters and wrap values in double quotes with proper escaping, and a formatTagsArray() helper to escape each tag individually. Apply the same pattern here.

🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/qoder.ts` around lines 22 - 33,
formatFile currently interpolates CommandContent.name, description, category and
tags directly into YAML which can break when values contain special characters;
update formatFile (in src/core/command-generation/adapters/qoder.ts) to use the
same helpers as claude/windsurf: call escapeYamlValue() for name, description
and category and use formatTagsArray() (or escapeYamlValue per-tag) to produce a
safely escaped tags array string, ensuring double-quoting and backslash-escaping
of quotes/newlines as those helpers do; preserve the same frontmatter layout but
replace raw interpolations with the escaped values and ensure tagsStr is built
from escaped tags.

Comment on lines +22 to +28
formatFile(content: CommandContent): string {
return `description = "${content.description}"
prompt = """
${content.body}
"""
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape TOML string values to avoid invalid output.
description is inserted into a TOML basic string without escaping, so quotes/backslashes/newlines will break the file.

🐛 Suggested fix
+function escapeTomlBasicString(value: string): string {
+  return value
+    .replace(/\\/g, '\\\\')
+    .replace(/"/g, '\\"')
+    .replace(/\r/g, '\\r')
+    .replace(/\n/g, '\\n');
+}
+
 export const qwenAdapter: ToolCommandAdapter = {
   toolId: 'qwen',
@@
   formatFile(content: CommandContent): string {
-    return `description = "${content.description}"
+    return `description = "${escapeTomlBasicString(content.description)}"
 
 prompt = """
 ${content.body}
 """
 `;
   },
 };
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/qwen.ts` around lines 22 - 28,
formatFile inserts content.description directly into a TOML basic string which
can produce invalid TOML if it contains quotes, backslashes, or newlines; update
formatFile (in the qwen adapter) to run content.description through a
TOML-escaping helper (e.g., escapeTomlBasicString or similar) that escapes
backslashes and double quotes and encodes control/newline characters before
interpolation, then use the escaped string in the description line; reference
the formatFile method and the CommandContent.description field when making the
change.

@TabishB TabishB merged commit d485281 into main Jan 22, 2026
9 checks passed
@TabishB TabishB deleted the multi-provider-skill-generation branch January 22, 2026 07:32
@diegohb
Copy link

diegohb commented Jan 25, 2026

@TabishB - have you considered openpackage.dev (enulus/openpackage) to avoid dealing with how to best support and transform for each tool? if not, how come? I'm trying to decide if that's a good tool to tie myself to.

@TabishB
Copy link
Contributor Author

TabishB commented Jan 25, 2026

@diegohb because I've never heard of the package and when I building OpenSpec in August that package didn't exist.

It might make sense for you, but I can't say for sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants