Skip to content

fix(qwen): generate Markdown commands instead of deprecated TOML format#1191

Open
mc856 wants to merge 2 commits into
Fission-AI:mainfrom
mc856:fix/qwen-markdown-commands
Open

fix(qwen): generate Markdown commands instead of deprecated TOML format#1191
mc856 wants to merge 2 commits into
Fission-AI:mainfrom
mc856:fix/qwen-markdown-commands

Conversation

@mc856

@mc856 mc856 commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Qwen Code has deprecated TOML custom commands in favor of Markdown files with YAML frontmatter (docs). This PR updates the Qwen adapter to emit .qwen/commands/opsx-<id>.md and registers the old opsx-*.toml files as legacy artifacts so they are cleaned up on update.

Closes #838

Changes

  • qwen adapter now generates Markdown with description frontmatter (YAML values escaped, same as the pi/claude adapters)
  • legacy-cleanup gains a .qwen/commands/opsx-*.toml pattern, following the junie/opencode precedent
  • Tests updated for the new format, plus a YAML-escaping test and a negative test ensuring new .md files are not flagged as legacy
  • docs/supported-tools.md and changeset updated

Design decisions

  • Args handling: the Markdown format supports {{args}}, but the old TOML adapter relied on Qwen's default argument appending, so this PR intentionally preserves that behavior for parity. Explicit {{args}} injection could be a follow-up.
  • Migration path: existing .qwen/commands/opsx-*.toml files are cleaned up via the same legacy-cleanup mechanism used for the junie/opencode migrations (interactive confirm on update, auto with --force). Non-interactive runs without --force warn and leave the old files in place, matching existing behavior.

Testing

  • npx vitest run — full suite passes (1664 tests)
  • npx eslint clean on changed files

Generated with Claude (Cowork) using claude-fable-5.

Summary by CodeRabbit

  • New Features

    • Qwen Code commands now use Markdown files with YAML frontmatter instead of TOML.
  • Bug Fixes

    • Improved detection and cleanup of legacy TOML command artifacts during updates.
  • Documentation

    • Updated docs to reflect the new Markdown-based Qwen command format and paths.
  • Tests

    • Test coverage updated to validate Markdown/YAML output and legacy detection behavior.

Qwen Code deprecated TOML custom commands in favor of Markdown files
with YAML frontmatter. Update the Qwen adapter to emit
.qwen/commands/opsx-<id>.md and register the old opsx-*.toml files as
legacy artifacts so they are cleaned up on update.

Closes Fission-AI#838

Generated with Claude (Cowork) using claude-fable-5; tested with the
full vitest suite (1663 tests passing).
@mc856 mc856 requested a review from TabishB as a code owner June 9, 2026 19:01
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 76780d06-5164-4368-8faa-ce336ef25de3

📥 Commits

Reviewing files that changed from the base of the PR and between c19d629 and 930de0c.

📒 Files selected for processing (1)
  • test/core/command-generation/registry.test.ts

📝 Walkthrough

Walkthrough

This PR migrates Qwen command generation from deprecated TOML format to Markdown with YAML frontmatter, aligning with Qwen Code's current recommendations. The adapter gains a YAML-safe value escaper, legacy TOML artifacts are marked for cleanup detection, and tests are updated to validate the new format and proper character escaping.

Changes

Qwen Markdown Command Format Migration

Layer / File(s) Summary
Markdown adapter with YAML frontmatter
src/core/command-generation/adapters/qwen.ts, test/core/command-generation/adapters.test.ts
Qwen adapter now generates .md files instead of .toml. A new escapeYamlValue helper safely quotes description values with YAML-special characters. Module docs and tests updated to validate YAML frontmatter output with proper escaping.
Legacy TOML cleanup infrastructure
src/core/legacy-cleanup.ts, test/core/legacy-cleanup.test.ts
Legacy command path detection expanded to match both opsx-*.toml and openspec-*.toml patterns. Tests confirm old TOML files are flagged as legacy while new Markdown files are not.
Documentation and integration updates
.changeset/qwen-markdown-commands.md, docs/supported-tools.md, test/core/update.test.ts, test/core/command-generation/registry.test.ts
Changeset documents the format migration. Supported tools table shows .md extension. Integration and registry tests validate generated commands produce Markdown with YAML frontmatter delimiters and description: fields and adjust the no-YAML-frontmatter allowlist.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Fission-AI/OpenSpec#250: Adds Qwen slash command configurators that generate .qwen/commands entries with YAML frontmatter, directly aligning with this PR's format migration.
  • Fission-AI/OpenSpec#317: Previous Qwen command-config generation logic that this PR replaces by switching from TOML to Markdown format with YAML frontmatter.
  • Fission-AI/OpenSpec#760: Related legacy-cleanup changes that allow LEGACY_SLASH_COMMAND_PATHS.pattern to be a string or array, which this PR builds upon.

Suggested reviewers

  • TabishB
  • alfred-openspec

Poem

🐰 From TOML burrows deep to Markdown's sunny glade,
YAML ribbons wrap each command we've made,
I nibble at edge-cases, escape every quote,
Now Qwen's new commands proudly skip the old note—
Hop, code, hop! The format's fresh and ready to tote.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: updating Qwen adapter to generate Markdown commands instead of deprecated TOML format, which aligns with the primary objective.
Linked Issues check ✅ Passed All coding requirements from issue #838 are met: file path changed to .md, TOML replaced with Markdown YAML frontmatter, YAML values escaped, and legacy cleanup implemented.
Out of Scope Changes check ✅ Passed All changes directly support the migration from TOML to Markdown format and legacy cleanup; no unrelated modifications detected.
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 unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/command-generation/adapters/qwen.ts`:
- Around line 18-27: The escapeYamlValue function correctly detects values
needing quotes but doesn't escape carriage returns; update the escaping step in
escapeYamlValue to also replace '\r' with '\\r' (in addition to existing
backslash, double-quote, and newline escapes) so double-quoted YAML strings
conform to YAML 1.2; modify the replace chain inside escapeYamlValue
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1c9ab7b-d581-4492-929b-bc9c6e993f95

📥 Commits

Reviewing files that changed from the base of the PR and between 1b06fdd and c19d629.

📒 Files selected for processing (7)
  • .changeset/qwen-markdown-commands.md
  • docs/supported-tools.md
  • src/core/command-generation/adapters/qwen.ts
  • src/core/legacy-cleanup.ts
  • test/core/command-generation/adapters.test.ts
  • test/core/legacy-cleanup.test.ts
  • test/core/update.test.ts

Comment on lines +18 to +27
function escapeYamlValue(value: string): string {
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}

@coderabbitai coderabbitai Bot Jun 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add carriage return escape sequence for complete YAML compliance.

The regex on line 20 correctly detects \r as requiring quotes, but line 23 doesn't escape it. Per the YAML 1.2 specification, carriage returns inside double-quoted strings must be escaped. While carriage returns are rare in description strings, adding the escape ensures valid YAML output.

🛡️ Proposed fix to add carriage return escaping
   // Use double quotes and escape internal double quotes and backslashes
-  const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
+  const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
   return `"${escaped}"`;
📝 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
function escapeYamlValue(value: string): string {
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}
function escapeYamlValue(value: string): string {
// Check if value needs quoting (contains special YAML characters or starts/ends with whitespace)
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
// Use double quotes and escape internal double quotes and backslashes
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
return `"${escaped}"`;
}
return value;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/command-generation/adapters/qwen.ts` around lines 18 - 27, The
escapeYamlValue function correctly detects values needing quotes but doesn't
escape carriage returns; update the escaping step in escapeYamlValue to also
replace '\r' with '\\r' (in addition to existing backslash, double-quote, and
newline escapes) so double-quoted YAML strings conform to YAML 1.2; modify the
replace chain inside escapeYamlValue accordingly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch on the detect/escape asymmetry. This function is an intentional verbatim copy of the existing escapeYamlValue in pi.ts and claude.ts (current convention is one copy per adapter), so the same gap exists in both siblings. Since descriptions come from internal command templates, a literal CR can't occur in practice. Rather than diverging this copy from the other two or expanding this PR's scope, I'd prefer a follow-up PR that extracts escapeYamlValue into a shared util and adds \r escaping in one place. Happy to fix it here instead if maintainers prefer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mc856, that context makes sense — the detect/escape asymmetry is a pre-existing gap shared across all three adapters, and fixing it consistently via a shared utility is the right approach. Happy to leave this PR as-is.

Would you like me to open a follow-up GitHub issue to track:

  • Extracting escapeYamlValue into a shared YAML utility
  • Adding \r → \\r escaping in that shared location
  • Updating pi.ts, claude.ts, and qwen.ts to consume it

Just let me know and I'll file it.


✏️ Learnings added
Learnt from: mc856
Repo: Fission-AI/OpenSpec PR: 1191
File: src/core/command-generation/adapters/qwen.ts:18-27
Timestamp: 2026-06-09T19:17:10.070Z
Learning: In the OpenSpec repository, `escapeYamlValue` is intentionally duplicated per adapter file (e.g., `src/core/command-generation/adapters/pi.ts`, `claude.ts`, `qwen.ts`) — one copy per adapter is the current convention. There is a known detect/escape asymmetry where `\r` is detected as needing quoting but is not escaped in the replace chain; this gap exists in all three siblings and is tracked for a future refactor that would extract the helper into a shared util.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@coderabbitai yes, please file the follow-up issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

gh issue create \
  --repo Fission-AI/OpenSpec \
  --title "refactor(adapters): extract escapeYamlValue into shared util and fix \\r escape gap" \
  --assignee mc856 \
  --body "## Summary

The \`escapeYamlValue\` helper is currently duplicated verbatim across three adapter files:

- \`src/core/command-generation/adapters/pi.ts\`
- \`src/core/command-generation/adapters/claude.ts\`
- \`src/core/command-generation/adapters/qwen.ts\`

There is also a known detect/escape asymmetry: \`\\r\` (carriage return) is correctly detected as requiring YAML quoting (via the regex), but is **not** escaped in the double-quoted string replace chain. This gap exists consistently across all three siblings.

## Proposed Work

1. Extract \`escapeYamlValue\` into a shared utility (e.g., \`src/core/command-generation/utils/yaml.ts\` or similar).
2. Add \`\r → \\\\r\` escaping in the shared implementation so double-quoted YAML strings fully conform to YAML 1.2.
3. Update \`pi.ts\`, \`claude.ts\`, and \`qwen.ts\` to import and use the shared helper instead of their local copies.
4. Add or update unit tests for the shared utility to cover the \`\\r\` case.

## Context

- Discussed in PR `#1191` (comment: https://github.com/Fission-AI/OpenSpec/pull/1191#discussion_r3383171623)
- The \`\\r\` fix was intentionally deferred from PR `#1191` to avoid diverging the qwen adapter copy from its siblings before a proper shared-util refactor.

**Requested by:** `@mc856`
"

Length of output: 3193


🏁 Script executed:

gh issue create \
  --repo Fission-AI/OpenSpec \
  --title "refactor(adapters): extract escapeYamlValue into shared util and fix \\r escape gap" \
  --body "## Summary

The \`escapeYamlValue\` helper is currently duplicated verbatim across three adapter files:

- \`src/core/command-generation/adapters/pi.ts\`
- \`src/core/command-generation/adapters/claude.ts\`
- \`src/core/command-generation/adapters/qwen.ts\`

There is also a known detect/escape asymmetry: \`\\r\` (carriage return) is correctly detected as requiring YAML quoting (via the regex), but is **not** escaped in the double-quoted string replace chain. This gap exists consistently across all three siblings.

## Proposed Work

1. Extract \`escapeYamlValue\` into a shared utility (e.g., \`src/core/command-generation/utils/yaml.ts\` or similar).
2. Add \`\r → \\\\r\` escaping in the shared implementation so double-quoted YAML strings fully conform to YAML 1.2.
3. Update \`pi.ts\`, \`claude.ts\`, and \`qwen.ts\` to import and use the shared helper instead of their local copies.
4. Add or update unit tests for the shared utility to cover the \`\\r\` case.

## Context

- Discussed in PR `#1191` (comment: https://github.com/Fission-AI/OpenSpec/pull/1191#discussion_r3383171623)
- The \`\\r\` fix was intentionally deferred from PR `#1191` to avoid diverging the qwen adapter copy from its siblings before a proper shared-util refactor.

**Requested by:** `@mc856`
"

Length of output: 1527



@mc856, the follow-up issue has been filed: #1205

It tracks:

  1. Extracting escapeYamlValue into a shared utility (e.g., src/core/command-generation/utils/yaml.ts)
  2. Adding \r → \\r escaping in that shared implementation for YAML 1.2 compliance
  3. Updating pi.ts, claude.ts, and qwen.ts to consume the shared helper
  4. Adding/updating unit tests to cover the \r case

Note: the integration couldn't set the assignee automatically — you may want to self-assign it via the GitHub UI.


🧬 Code Graph Analysis Results

Relevant code snippets to understand src/core/command-generation/adapters/qwen.ts and its inputs/outputs:

  1. src/core/command-generation/types.ts (lines 12-25)
export interface CommandContent {
  /** Command identifier (e.g., 'explore', 'apply', 'new') */
  id: string;
  /** Human-readable name (e.g., 'OpenSpec Explore') */
  name: string;
  /** Brief description of command purpose */
  description: string;
  /** Grouping category (e.g., 'Workflow') */
  category: string;
  /** Array of tag strings */
  tags: string[];
  /** The command instruction content (body text) */
  body: string;
}
  1. src/core/command-generation/types.ts (lines 32-48)
export interface ToolCommandAdapter {
  /** Tool identifier matching AIToolOption.value (e.g., 'claude', 'cursor') */
  toolId: string;
  /**
   * Returns the file path for a command.
   * `@param` commandId - The command identifier (e.g., 'explore')
   * `@returns` Path from project root (e.g., '.claude/commands/opsx/explore.md').
   *          May be absolute for tools with global-scoped prompts (e.g., Codex).
   */
  getFilePath(commandId: string): string;
  /**
   * Formats the complete file content including frontmatter.
   * `@param` content - The tool-agnostic command content
   * `@returns` Complete file content ready to write
   */
  formatFile(content: CommandContent): string;
}

🧠 Learnings used
Learnt from: mc856
Repo: Fission-AI/OpenSpec PR: 1191
File: src/core/command-generation/adapters/qwen.ts:18-27
Timestamp: 2026-06-09T19:17:10.070Z
Learning: In this repo, `escapeYamlValue` is intentionally duplicated in each adapter file (one implementation per adapter) rather than extracted to a shared util. When reviewing adapter files under `src/core/command-generation/adapters/`, do not require deduplication/refactoring of `escapeYamlValue` across siblings. Also note the known detect/escape asymmetry for `\r` (it is detected as needing quoting but is not escaped in the current replace chain) and that this behavior currently exists consistently across the adapter siblings; treat this as an existing, tracked gap and don’t insist on fixing it unless the change is part of the planned refactor and updated consistently for all adapters.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, this is the right fix shape. I checked the Qwen docs, current adapter, legacy cleanup, docs, and tests, and the migration to Markdown commands plus TOML cleanup lines up.

One tiny follow-up: registry.test.ts still lists qwen with non-YAML tools, but the adapter/update coverage here makes that non-blocking.

@mc856

mc856 commented Jun 12, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Good point on registry.test.ts — pushed 930de0c removing qwen from the noYamlFrontmatter list, so the registry-level test now asserts frontmatter for qwen as well. The new commit auto-dismissed the approval; re-approval appreciated when you get a chance.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approved latest head 930de0c. I verified the follow-up is the requested registry-test change removing qwen from the non-YAML-frontmatter allowlist, and reran the focused adapter/registry/legacy-cleanup/update tests in a temp worktree: 252 tests passed. The shared escape helper cleanup can stay a non-blocking follow-up.

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.

OpenSpec generates deprecated TOML commands for Qwen Code

2 participants