Skip to content

Conversation

@TabishB
Copy link
Contributor

@TabishB TabishB commented Jan 23, 2026

Summary

  • Merges the experimental workflow into the main init command
  • Removes legacy configurators and templates (ToolRegistry, agent configs, slash command generators)
  • Moves workflow commands from experimental/ to workflow/ directory
  • Updates openspec update to refresh skills/commands instead of config files
  • Adds legacy detection and cleanup for migrating existing projects

Breaking Changes

  • openspec init now uses the skill-based workflow (previously in experimental)
  • Legacy config files (AGENTS.md, tool configs) are detected and can be cleaned up
  • The experimental command is now a hidden alias to init

Test plan

  • Integration tests for new init flow (fresh install)
  • Integration tests for legacy detection and cleanup
  • Integration tests for extend mode (re-running init)
  • Non-interactive mode with --tools flag
  • --force flag for CI environments
  • Full test suite passes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced new OPSX workflow with interactive tool selection during initialization.
    • Added new CLI commands: status, instructions, templates, schemas, and new change for managing OpenSpec workflows.
    • Enhanced init with --force option for automated legacy cleanup.
  • Documentation

    • Updated README with new initialization flow and OPSX-based slash commands.
    • Refreshed documentation to reflect tool selection and configuration workflows.

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

Implement src/core/legacy-cleanup.ts with detection and cleanup functions
for all legacy OpenSpec artifact types:

Detection functions:
- detectLegacyConfigFiles() - checks for config files with OpenSpec markers
  (CLAUDE.md, CLINE.md, CODEBUDDY.md, COSTRICT.md, QODER.md, IFLOW.md,
  AGENTS.md, QWEN.md)
- detectLegacySlashCommands() - checks for old /openspec:* command
  directories and files across all 21 tool integrations
- detectLegacyStructureFiles() - checks for openspec/AGENTS.md and
  openspec/project.md (project.md preserved for migration hint)
- detectLegacyArtifacts() - orchestrates all detection

Utility functions:
- hasOpenSpecMarkers() - checks if content has OpenSpec markers
- isOnlyOpenSpecContent() - checks if file is 100% OpenSpec content
- removeMarkerBlock() - surgically removes marker blocks from mixed content

Cleanup functions:
- cleanupLegacyArtifacts() - orchestrates removal with proper edge cases:
  - Deletes files that are 100% OpenSpec content
  - Removes marker blocks from files with mixed content
  - Deletes legacy slash command directories and files
  - Preserves openspec/project.md (shows migration hint only)

Formatting functions:
- formatDetectionSummary() - formats what was detected before cleanup
- formatCleanupSummary() - formats what was cleaned up after

This is task 1.1 for the merge-init-experimental change.
…locks

- Add removeMarkerBlock() function to file-system.ts that properly handles
  inline marker mentions by using findMarkerIndex/isMarkerOnOwnLine
- Refactor legacy-cleanup.ts to use the shared utility
- Export removeMarkerBlock from utils/index.ts for reusability
- Add comprehensive tests for inline marker mention edge cases
- Add tests for shell-style markers and various whitespace scenarios

The new implementation correctly ignores markers mentioned inline within
text and only removes actual marker blocks that are on their own lines.
- Add standalone formatProjectMdMigrationHint() function for reusable
  migration hint output directing users to migrate project.md content
  to config.yaml's "context:" field
- Update formatDetectionSummary() to include the migration hint when
  project.md is detected (not just in cleanup summary)
- Refactor formatCleanupSummary() to use the new function for
  consistency
- Add unit tests for the new function and updated behavior
Rewrites the init command tests to verify the new experimental workflow
implementation. The new tests cover:

- OpenSpec directory structure creation (specs, changes, archive)
- config.yaml generation with default schema
- 9 Agent Skills creation for various tools (Claude, Cursor, Windsurf, etc.)
- 9 slash commands generation using tool-specific adapters
- Multi-tool support (--tools all, --tools none, specific tools)
- Extend mode (re-running init)
- Tool-specific adapters (Gemini TOML, Continue .prompt, etc.)
- Error handling for invalid tools and permissions

Removes old tests for legacy config file generation (AGENTS.md, CLAUDE.md,
project.md, etc.) as the new init command uses Agent Skills instead.
Update the update command tests to match the new implementation that
refreshes skills and opsx commands instead of config files.

Changes:
- Remove old ToolRegistry import (deleted module)
- Rewrite tests to verify skill file updates
- Rewrite tests to verify opsx command generation
- Add tests for multi-tool support (Claude, Cursor, Qwen, Windsurf)
- Add tests for error handling and tool detection
- Fix test assertions to match actual skill template names

The update command now:
- Detects configured tools by checking skill directories
- Updates SKILL.md files with latest skill templates
- Generates opsx commands using tool-specific adapters
- Replace tool list with simplified supported tools section (skills-based)
- Update init instructions to document --tools flag, --force, and legacy cleanup
- Replace project.md with config.yaml documentation
- Update workflow examples to use /opsx:* commands instead of /openspec:*
- Add command reference table for slash commands
- Update Team Adoption and Updating sections for new workflow
- Replace Experimental Features with Workflow Customization section
…o workflow

- Delete src/core/configurators/ directory (ToolRegistry, all config generators)
- Delete legacy templates (agents-template, claude-template, project-template, etc.)
- Move experimental commands to src/commands/workflow/ with cleaner structure
- Remove experimental setup.ts and index.ts (functionality merged into init)
- Update CLI to register workflow commands directly instead of through experimental
- Update openspec update command to refresh skills/commands instead of config files
- Update tests for new command structure
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

This PR fundamentally restructures OpenSpec's architecture by replacing a registry-based configurator pattern for AI tools and slash commands with a skills-directory approach, consolidating experimental artifact workflows into primary CLI commands, and introducing legacy artifact cleanup capabilities. Major refactoring of init/update flows and comprehensive test updates accompany these changes.

Changes

Cohort / File(s) Summary
Documentation & Configuration
README.md, openspec/changes/merge-init-experimental/tasks.md
Updated documentation to reflect OPSX workflow terminology, new config.yaml customization, and streamlined command references; marked legacy init tasks as completed.
CLI Core Refactoring
src/cli/index.ts
Consolidated workflow commands from experimental namespace into primary CLI; added tool discovery via skillsDir filtering; introduced --force option for init; added hidden experimental alias for backward compatibility.
Experimental Module Removal
src/commands/experimental/index.ts, src/commands/experimental/setup.ts
Removed entire experimental artifact workflow command module and interactive setup orchestration (tool selection, skill/command generation, config creation).
Workflow Command Consolidation
src/commands/workflow/index.ts
New index module re-exporting status, instructions, templates, schemas, new-change commands and their option types from dedicated submodules.
Configurator & Registry Removal
src/core/configurators/base.ts, src/core/configurators/registry.ts, src/core/configurators/agents.ts, src/core/configurators/claude.ts, src/core/configurators/cline.ts, src/core/configurators/codebuddy.ts, src/core/configurators/costrict.ts, src/core/configurators/iflow.ts, src/core/configurators/qoder.ts, src/core/configurators/qwen.ts
Removed entire tool configurator infrastructure including ToolConfigurator base interface and concrete implementations for multiple AI tools; removed ToolRegistry.
Slash Command Configurator Removal
src/core/configurators/slash/base.ts, src/core/configurators/slash/registry.ts, src/core/configurators/slash/amazon-q.ts, src/core/configurators/slash/antigravity.ts, src/core/configurators/slash/auggie.ts, src/core/configurators/slash/claude.ts, src/core/configurators/slash/cline.ts, src/core/configurators/slash/codebuddy.ts, src/core/configurators/slash/codex.ts, src/core/configurators/slash/continue.ts, src/core/configurators/slash/costrict.ts, src/core/configurators/slash/crush.ts, src/core/configurators/slash/cursor.ts, src/core/configurators/slash/factory.ts, src/core/configurators/slash/gemini.ts, src/core/configurators/slash/github-copilot.ts, src/core/configurators/slash/iflow.ts, src/core/configurators/slash/kilocode.ts, src/core/configurators/slash/opencode.ts, src/core/configurators/slash/qoder.ts, src/core/configurators/slash/qwen.ts, src/core/configurators/slash/roocode.ts, src/core/configurators/slash/toml-base.ts, src/core/configurators/slash/windsurf.ts
Removed abstract SlashCommandConfigurator base class, concrete implementations for 22+ tools, and SlashCommandRegistry; eliminates entire slash-command configuration infrastructure.
Template System Refactoring
src/core/templates/agents-root-stub.ts, src/core/templates/agents-template.ts, src/core/templates/claude-template.ts, src/core/templates/cline-template.ts, src/core/templates/costrict-template.ts, src/core/templates/index.ts, src/core/templates/project-template.ts, src/core/templates/slash-command-templates.ts
Removed legacy template exports (agentsTemplate, projectTemplate, SlashCommandId, TemplateManager); consolidated re-exports now point to skill-templates module with new getXSkillTemplate functions.
Core Logic Overhaul
src/core/init.ts, src/core/init/wizard.ts, src/core/legacy-cleanup.ts, src/core/update.ts
Major refactor of InitCommand with legacy cleanup detection/handling, tool-state awareness, skill/command generation, and config creation; removed interactive wizard; added new legacy-cleanup module for artifact detection/remediation; refactored update flow to use skill detection instead of registries.
Utility Enhancements
src/utils/file-system.ts, src/utils/index.ts
Added removeMarkerBlock utility function for extracting marker-wrapped content blocks; re-exported from utils index.
Test Overhaul
test/cli-e2e/basic.test.ts, test/commands/artifact-workflow.test.ts, test/core/init.test.ts, test/core/legacy-cleanup.test.ts, test/core/update.test.ts, test/utils/marker-updates.test.ts
Comprehensive test rewrite: replaced artifact-based assertions with SKILL.md checks; removed wizard/interactive prompt mocking; added legacy-cleanup test suite; refactored init/update tests to cover non-interactive and multi-tool flows; added removeMarkerBlock tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

This PR involves substantial architectural restructuring across multiple subsystems: elimination of the configurator/registry pattern (30+ files removed), introduction of new legacy-cleanup infrastructure, major refactoring of init/update control flows with new public APIs, template system consolidation, and comprehensive test rewrites. The high heterogeneity of changes (removals, additions, refactors across different domains) combined with logic density in core initialization/update modules and the need to verify how all restructured pieces integrate together demands careful, methodical review.

Possibly related PRs

  • PR #442: Adds slash-command generation to experimental setup flow; related to this PR's integration of skills + slash-command generation into init/workflow.
  • PR #564: Implements merge of experimental into init (legacy cleanup, skill-based approach); directly aligns with this PR's architectural goals and restructuring.
  • PR #556: Adds multi-provider skill/command generation (skillsDir approach, adapter-based commands); foundational work that this PR builds upon and consolidates.

Suggested reviewers

  • Israel-Laguan

🐰 Hopping through the halls of code,
Old registries transformed to skill-based roads,
Legacy whispers swept away with care,
New workflows dance through cleaner air!

🚥 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): merge init and experimental commands' is concise and accurately summarizes the main change—merging two CLI commands as described throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% 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.

@greptile-apps
Copy link

greptile-apps bot commented Jan 23, 2026

Greptile Summary

This PR successfully merges the experimental workflow into the main init command, unifying the setup experience while maintaining backward compatibility. The refactoring removes the legacy tool configurator system (ToolRegistry, wizard-based prompts) and replaces it with a cleaner skill-based approach that generates Agent Skills and /opsx:* slash commands directly.

Key changes:

  • openspec init now uses the skill-based workflow (previously experimental)
  • Removed ~3500 lines of legacy code (configurators, templates, wizard)
  • Added comprehensive legacy artifact detection and cleanup with user prompts
  • openspec update now refreshes skills/commands instead of config files
  • experimental command becomes a hidden alias for backward compatibility
  • Workflow commands (status, instructions, etc.) moved from experimental/ to top-level

Migration safety:

  • Legacy files are detected automatically during init
  • Interactive mode prompts for cleanup confirmation
  • --force flag allows automatic cleanup in CI environments
  • Non-interactive mode without --force will abort if legacy files are detected

Testing:
Comprehensive test coverage for the new init flow, legacy cleanup logic, and update command. Tests cover fresh installs, extend mode, multiple tool configurations, and various legacy artifact scenarios.

Confidence Score: 4/5

  • Safe to merge with minor testing recommendations
  • Score of 4 reflects a well-executed refactoring with comprehensive test coverage and careful migration handling. The breaking changes are documented, legacy cleanup is interactive with safeguards, and the test suite covers the main scenarios. Slight deduction for the scope of changes (3500+ line removal) and potential edge cases in legacy detection across different tool configurations.
  • Pay attention to src/core/init.ts during integration testing with multiple tools to ensure skill generation works consistently across all supported AI tools. Verify legacy cleanup behavior with real-world legacy configurations.

Important Files Changed

Filename Overview
src/core/init.ts Completely rewritten to merge experimental workflow into main init command, replaces wizard-based tool registry with skill-based approach, adds legacy cleanup handling
src/core/legacy-cleanup.ts New module for detecting and cleaning up legacy OpenSpec artifacts (config files, slash commands, AGENTS.md), well-structured with comprehensive detection logic
src/core/update.ts Simplified to refresh skills/commands for configured tools instead of regenerating legacy config files, clean implementation
src/cli/index.ts Adds experimental as hidden alias to init with deprecation notice, moves workflow commands from experimental/ to top-level, adds --force flag for init
test/core/init.test.ts Rewritten to test new skill-based init flow with --tools flag, tests fresh install, extend mode, and multiple tool setup
test/core/legacy-cleanup.test.ts New comprehensive test suite for legacy artifact detection and cleanup, covers marker detection, removal logic, and cleanup operations
README.md Updated documentation to reflect skill-based workflow as the main approach, removes experimental distinction

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI (index.ts)
    participant Init as InitCommand
    participant Legacy as legacy-cleanup
    participant FS as FileSystem
    participant Skills as Skill Generation

    User->>CLI: openspec init --tools claude
    CLI->>Init: execute(targetPath)
    
    Init->>FS: Check if openspec/ exists
    FS-->>Init: extendMode = true/false
    
    Init->>Legacy: detectLegacyArtifacts()
    Legacy->>FS: Check for config files (CLAUDE.md, AGENTS.md, etc.)
    Legacy->>FS: Check for slash command dirs (.claude/commands/openspec/, etc.)
    Legacy->>FS: Check for openspec/AGENTS.md
    Legacy-->>Init: LegacyDetectionResult
    
    alt Has legacy artifacts && interactive
        Init->>User: Prompt: "Upgrade and clean up legacy files?"
        User-->>Init: Confirm
        Init->>Legacy: cleanupLegacyArtifacts()
        Legacy->>FS: Delete/modify legacy files
        Legacy-->>Init: CleanupResult
    end
    
    Init->>Init: getSelectedTools() with --tools flag
    Init->>Init: validateTools()
    
    Init->>FS: Create openspec/ structure
    FS->>FS: mkdir specs, changes, changes/archive
    
    Init->>Skills: generateSkillsAndCommands(claude)
    Skills->>FS: Write .claude/skills/openspec-*/SKILL.md (9 files)
    Skills->>FS: Write .claude/commands/opsx/*.md (9 files)
    Skills-->>Init: Results
    
    Init->>FS: Create openspec/config.yaml (if not exists)
    
    Init->>User: Display success message
    User->>User: Restart IDE for skills to load
Loading

@vibe-kanban-cloud
Copy link

Review Complete

Your review story is ready!

View Story

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

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: 6

🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 262-264: Remove the shell prompt characters from the example
commands so markdownlint rule MD014 passes: edit the README.md code block lines
that currently contain "$ openspec list", "$ openspec validate
add-profile-filters", and "$ openspec show add-profile-filters" (and the similar
occurrence around line 288) to be plain commands "openspec list", "openspec
validate add-profile-filters", and "openspec show add-profile-filters" without
the leading "$ ".

In `@src/core/init.ts`:
- Around line 682-686: The status message always prints "openspec/config.yaml"
even when only openspec/config.yml exists; update the logging in init.ts (the
block using configStatus, DEFAULT_SCHEMA) to print the actual filename present:
query the result from createConfig() (or check fs.existsSync for
"openspec/config.yaml" vs "openspec/config.yml") and use that filename in the
console.log for both the 'created' and 'exists' branches so the message
accurately reflects the real file name.

In `@src/core/legacy-cleanup.ts`:
- Around line 129-136: The migration hint is not shown when only
openspec/project.md exists because result.hasLegacyArtifacts is computed without
including result.hasProjectMd; update the assignment that sets
result.hasLegacyArtifacts (the boolean expression combining result.configFiles,
result.slashCommandDirs, result.slashCommandFiles, result.hasOpenspecAgents,
result.hasRootAgentsWithMarkers) to also OR in result.hasProjectMd so the
presence of openspec/project.md triggers the migration hint.
- Around line 436-458: The code double-processes the root AGENTS.md and can
attempt to read/delete it after the legacy config cleanup already removed it;
update the AGENTS.md handling block (guarded by
detection.hasRootAgentsWithMarkers) to first verify the file still exists (e.g.,
via FileSystemUtils.exists or equivalent) before calling
FileSystemUtils.readFile/remove/write; reference the rootAgentsPath variable and
functions isOnlyOpenSpecContent, removeMarkerBlock, FileSystemUtils.readFile and
FileSystemUtils.writeFile so the check prevents attempts to process a file that
was already deleted and avoids the spurious "Failed to handle AGENTS.md" errors.

In `@src/utils/file-system.ts`:
- Around line 264-303: The function removeMarkerBlock currently uses trim()
which removes leading whitespace and normalizes line endings; change it to
preserve leading indentation and original newline style by replacing trim() with
trimEnd() (so leading spaces remain) and detect the file's newline sequence
(e.g., const newline = content.includes('\r\n') ? '\r\n' : '\n') then return
result.trimEnd() === '' ? '' : result.trimEnd() + newline; update references in
removeMarkerBlock and keep findMarkerIndex usage unchanged so only trailing
whitespace/newline handling is modified.

In `@test/core/legacy-cleanup.test.ts`:
- Around line 845-856: Replace the hardcoded expectedTools array with a dynamic
list derived from CommandAdapterRegistry.getAll(): call
CommandAdapterRegistry.getAll(), map to each adapter's id (or adapter.id) to
build expected IDs, then assert parity with LEGACY_SLASH_COMMAND_PATHS by
comparing keys (e.g., ensure every id exists in LEGACY_SLASH_COMMAND_PATHS and
lengths match or assert exact set equality). Refer to
CommandAdapterRegistry.getAll() and LEGACY_SLASH_COMMAND_PATHS (and adapter.id)
when locating the code to update.
🧹 Nitpick comments (5)
src/core/update.ts (1)

42-118: DRY up skill directory names to avoid drift.
SKILL_NAMES duplicates the dirName list in skillTemplates. If one list changes, detection and update can diverge. Consider a shared SKILL_TEMPLATES constant to drive both.

♻️ Suggested refactor
-const SKILL_NAMES = [
-  'openspec-explore',
-  'openspec-new-change',
-  'openspec-continue-change',
-  'openspec-apply-change',
-  'openspec-ff-change',
-  'openspec-sync-specs',
-  'openspec-archive-change',
-  'openspec-bulk-archive-change',
-  'openspec-verify-change',
-];
+const SKILL_TEMPLATES = [
+  { dirName: 'openspec-explore', getTemplate: getExploreSkillTemplate },
+  { dirName: 'openspec-new-change', getTemplate: getNewChangeSkillTemplate },
+  { dirName: 'openspec-continue-change', getTemplate: getContinueChangeSkillTemplate },
+  { dirName: 'openspec-apply-change', getTemplate: getApplyChangeSkillTemplate },
+  { dirName: 'openspec-ff-change', getTemplate: getFfChangeSkillTemplate },
+  { dirName: 'openspec-sync-specs', getTemplate: getSyncSpecsSkillTemplate },
+  { dirName: 'openspec-archive-change', getTemplate: getArchiveChangeSkillTemplate },
+  { dirName: 'openspec-bulk-archive-change', getTemplate: getBulkArchiveChangeSkillTemplate },
+  { dirName: 'openspec-verify-change', getTemplate: getVerifyChangeSkillTemplate },
+];
+const SKILL_NAMES = SKILL_TEMPLATES.map((t) => t.dirName);
-    const skillTemplates = [
-      { template: getExploreSkillTemplate(), dirName: 'openspec-explore' },
-      { template: getNewChangeSkillTemplate(), dirName: 'openspec-new-change' },
-      { template: getContinueChangeSkillTemplate(), dirName: 'openspec-continue-change' },
-      { template: getApplyChangeSkillTemplate(), dirName: 'openspec-apply-change' },
-      { template: getFfChangeSkillTemplate(), dirName: 'openspec-ff-change' },
-      { template: getSyncSpecsSkillTemplate(), dirName: 'openspec-sync-specs' },
-      { template: getArchiveChangeSkillTemplate(), dirName: 'openspec-archive-change' },
-      { template: getBulkArchiveChangeSkillTemplate(), dirName: 'openspec-bulk-archive-change' },
-      { template: getVerifyChangeSkillTemplate(), dirName: 'openspec-verify-change' },
-    ];
+    const skillTemplates = SKILL_TEMPLATES.map(({ dirName, getTemplate }) => ({
+      dirName,
+      template: getTemplate(),
+    }));
openspec/changes/merge-init-experimental/tasks.md (1)

1-67: Consider archiving this completed change set.
All tasks are checked off. Per repo workflow, move this change bundle to openspec/changes/archive/2026-01-23-merge-init-experimental/ (and update specs accordingly) once merged. Based on learnings, ...

test/cli-e2e/basic.test.ts (1)

8-15: Consider using FileSystemUtils.fileExists instead of duplicating.

This local fileExists helper duplicates the static method in FileSystemUtils (see src/utils/file-system.ts lines 83-93). While minor, using the shared utility would reduce duplication.

♻️ Suggested refactor
 import { runCLI, cliProjectRoot } from '../helpers/run-cli.js';
 import { AI_TOOLS } from '../../src/core/config.js';
+import { FileSystemUtils } from '../../src/utils/file-system.js';
 
-async function fileExists(filePath: string): Promise<boolean> {
-  try {
-    await fs.access(filePath);
-    return true;
-  } catch {
-    return false;
-  }
-}
+const fileExists = FileSystemUtils.fileExists.bind(FileSystemUtils);
test/core/init.test.ts (1)

401-417: Consider extracting test helpers to a shared module.

The fileExists and directoryExists helpers are duplicated across test files (also in test/cli-e2e/basic.test.ts). Consider extracting to test/helpers/ for reuse.

test/core/update.test.ts (1)

24-26: Redundant vi.restoreAllMocks() in beforeEach.

vi.restoreAllMocks() is already called in afterEach (line 30), so calling it again in beforeEach is redundant for the first test. However, it doesn't cause issues and provides defense-in-depth if afterEach fails.

♻️ Optional cleanup
   beforeEach(async () => {
     // Create a temporary test directory
     testDir = path.join(os.tmpdir(), `openspec-test-${randomUUID()}`);
     await fs.mkdir(testDir, { recursive: true });
 
     // Create openspec directory
     const openspecDir = path.join(testDir, 'openspec');
     await fs.mkdir(openspecDir, { recursive: true });
 
     updateCommand = new UpdateCommand();
-
-    // Clear all mocks before each test
-    vi.restoreAllMocks();
   });

Comment on lines 262 to 264
$ openspec list # Confirm the change folder exists
$ openspec validate add-profile-filters # Validate spec formatting
$ openspec show add-profile-filters # Review proposal, tasks, and spec delta
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

Drop $ prompts in code blocks to satisfy MD014.
markdownlint flags prompts when no output is shown.

📝 Suggested edits
-$ openspec list                             # Confirm the change folder exists
-$ openspec validate add-profile-filters     # Validate spec formatting
-$ openspec show add-profile-filters         # Review proposal, tasks, and spec delta
+openspec list                             # Confirm the change folder exists
+openspec validate add-profile-filters     # Validate spec formatting
+openspec show add-profile-filters         # Review proposal, tasks, and spec delta
-$ openspec archive add-profile-filters --yes
+openspec archive add-profile-filters --yes

Also applies to: 288-288

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

262-262: Dollar signs used before commands without showing output

(MD014, commands-show-output)


263-263: Dollar signs used before commands without showing output

(MD014, commands-show-output)


264-264: Dollar signs used before commands without showing output

(MD014, commands-show-output)

🤖 Prompt for AI Agents
In `@README.md` around lines 262 - 264, Remove the shell prompt characters from
the example commands so markdownlint rule MD014 passes: edit the README.md code
block lines that currently contain "$ openspec list", "$ openspec validate
add-profile-filters", and "$ openspec show add-profile-filters" (and the similar
occurrence around line 288) to be plain commands "openspec list", "openspec
validate add-profile-filters", and "openspec show add-profile-filters" without
the leading "$ ".

Comment on lines +682 to +686
if (configStatus === 'created') {
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
} else if (configStatus === 'exists') {
console.log(`Config: openspec/config.yaml (exists)`);
} else {
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

Config status message is inaccurate when only config.yml exists.
createConfig() treats either file as “exists,” but the message always names config.yaml.

📝 Suggested edit
-      console.log(`Config: openspec/config.yaml (exists)`);
+      console.log('Config: openspec/config.yaml or config.yml (exists)');
🤖 Prompt for AI Agents
In `@src/core/init.ts` around lines 682 - 686, The status message always prints
"openspec/config.yaml" even when only openspec/config.yml exists; update the
logging in init.ts (the block using configStatus, DEFAULT_SCHEMA) to print the
actual filename present: query the result from createConfig() (or check
fs.existsSync for "openspec/config.yaml" vs "openspec/config.yml") and use that
filename in the console.log for both the 'created' and 'exists' branches so the
message accurately reflects the real file name.

Comment on lines +129 to +136
// Determine if any legacy artifacts exist
result.hasLegacyArtifacts =
result.configFiles.length > 0 ||
result.slashCommandDirs.length > 0 ||
result.slashCommandFiles.length > 0 ||
result.hasOpenspecAgents ||
result.hasRootAgentsWithMarkers;

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

Migration hint can be skipped when only openspec/project.md exists.
hasLegacyArtifacts doesn’t include hasProjectMd, so the init flow won’t surface the migration hint in that scenario.

🔧 Suggested fix
-  result.hasLegacyArtifacts =
-    result.configFiles.length > 0 ||
-    result.slashCommandDirs.length > 0 ||
-    result.slashCommandFiles.length > 0 ||
-    result.hasOpenspecAgents ||
-    result.hasRootAgentsWithMarkers;
+  result.hasLegacyArtifacts =
+    result.configFiles.length > 0 ||
+    result.slashCommandDirs.length > 0 ||
+    result.slashCommandFiles.length > 0 ||
+    result.hasOpenspecAgents ||
+    result.hasRootAgentsWithMarkers ||
+    result.hasProjectMd;
📝 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
// Determine if any legacy artifacts exist
result.hasLegacyArtifacts =
result.configFiles.length > 0 ||
result.slashCommandDirs.length > 0 ||
result.slashCommandFiles.length > 0 ||
result.hasOpenspecAgents ||
result.hasRootAgentsWithMarkers;
// Determine if any legacy artifacts exist
result.hasLegacyArtifacts =
result.configFiles.length > 0 ||
result.slashCommandDirs.length > 0 ||
result.slashCommandFiles.length > 0 ||
result.hasOpenspecAgents ||
result.hasRootAgentsWithMarkers ||
result.hasProjectMd;
🤖 Prompt for AI Agents
In `@src/core/legacy-cleanup.ts` around lines 129 - 136, The migration hint is not
shown when only openspec/project.md exists because result.hasLegacyArtifacts is
computed without including result.hasProjectMd; update the assignment that sets
result.hasLegacyArtifacts (the boolean expression combining result.configFiles,
result.slashCommandDirs, result.slashCommandFiles, result.hasOpenspecAgents,
result.hasRootAgentsWithMarkers) to also OR in result.hasProjectMd so the
presence of openspec/project.md triggers the migration hint.

Comment on lines +436 to +458
// Handle root AGENTS.md with OpenSpec markers
if (detection.hasRootAgentsWithMarkers) {
const rootAgentsPath = FileSystemUtils.joinPath(projectPath, 'AGENTS.md');
try {
const content = await FileSystemUtils.readFile(rootAgentsPath);

if (isOnlyOpenSpecContent(content)) {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
const newContent = removeMarkerBlock(content);
if (newContent === '') {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
await FileSystemUtils.writeFile(rootAgentsPath, newContent);
result.modifiedFiles.push('AGENTS.md');
}
}
} catch (error: any) {
result.errors.push(`Failed to handle AGENTS.md: ${error.message}`);
}
}
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

Avoid double-processing root AGENTS.md.
AGENTS.md is already handled via legacy config cleanup, then processed again here, which can surface false “failed to handle” errors after deletion.

🔧 Suggested fix
-  if (detection.hasRootAgentsWithMarkers) {
+  if (detection.hasRootAgentsWithMarkers && !detection.configFiles.includes('AGENTS.md')) {
📝 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
// Handle root AGENTS.md with OpenSpec markers
if (detection.hasRootAgentsWithMarkers) {
const rootAgentsPath = FileSystemUtils.joinPath(projectPath, 'AGENTS.md');
try {
const content = await FileSystemUtils.readFile(rootAgentsPath);
if (isOnlyOpenSpecContent(content)) {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
const newContent = removeMarkerBlock(content);
if (newContent === '') {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
await FileSystemUtils.writeFile(rootAgentsPath, newContent);
result.modifiedFiles.push('AGENTS.md');
}
}
} catch (error: any) {
result.errors.push(`Failed to handle AGENTS.md: ${error.message}`);
}
}
// Handle root AGENTS.md with OpenSpec markers
if (detection.hasRootAgentsWithMarkers && !detection.configFiles.includes('AGENTS.md')) {
const rootAgentsPath = FileSystemUtils.joinPath(projectPath, 'AGENTS.md');
try {
const content = await FileSystemUtils.readFile(rootAgentsPath);
if (isOnlyOpenSpecContent(content)) {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
const newContent = removeMarkerBlock(content);
if (newContent === '') {
await fs.unlink(rootAgentsPath);
result.deletedFiles.push('AGENTS.md');
} else {
await FileSystemUtils.writeFile(rootAgentsPath, newContent);
result.modifiedFiles.push('AGENTS.md');
}
}
} catch (error: any) {
result.errors.push(`Failed to handle AGENTS.md: ${error.message}`);
}
}
🤖 Prompt for AI Agents
In `@src/core/legacy-cleanup.ts` around lines 436 - 458, The code double-processes
the root AGENTS.md and can attempt to read/delete it after the legacy config
cleanup already removed it; update the AGENTS.md handling block (guarded by
detection.hasRootAgentsWithMarkers) to first verify the file still exists (e.g.,
via FileSystemUtils.exists or equivalent) before calling
FileSystemUtils.readFile/remove/write; reference the rootAgentsPath variable and
functions isOnlyOpenSpecContent, removeMarkerBlock, FileSystemUtils.readFile and
FileSystemUtils.writeFile so the check prevents attempts to process a file that
was already deleted and avoids the spurious "Failed to handle AGENTS.md" errors.

Comment on lines +264 to +303
export function removeMarkerBlock(
content: string,
startMarker: string,
endMarker: string
): string {
const startIndex = findMarkerIndex(content, startMarker);
const endIndex = startIndex !== -1
? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
: findMarkerIndex(content, endMarker);

if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
return content;
}

// Find the start of the line containing the start marker
let lineStart = startIndex;
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
lineStart--;
}

// Find the end of the line containing the end marker
let lineEnd = endIndex + endMarker.length;
while (lineEnd < content.length && content[lineEnd] !== '\n') {
lineEnd++;
}
// Include the trailing newline if present
if (lineEnd < content.length && content[lineEnd] === '\n') {
lineEnd++;
}

const before = content.substring(0, lineStart);
const after = content.substring(lineEnd);

// Clean up double blank lines
let result = before + after;
result = result.replace(/\n{3,}/g, '\n\n');

// Trim leading/trailing whitespace but preserve content
return result.trim() === '' ? '' : result.trim() + '\n';
}
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

Preserve leading whitespace and line endings when removing marker blocks.
trim() strips leading indentation and forces an LF newline, which can alter files with intentional leading whitespace or CRLF endings. Consider using trimEnd() and preserving the original newline style to avoid unintended diffs.

🛠️ Proposed adjustment
-  // Clean up double blank lines
-  let result = before + after;
-  result = result.replace(/\n{3,}/g, '\n\n');
-
-  // Trim leading/trailing whitespace but preserve content
-  return result.trim() === '' ? '' : result.trim() + '\n';
+  const newline = content.includes('\r\n') ? '\r\n' : '\n';
+
+  // Clean up double blank lines
+  let result = before + after;
+  result = result.replace(/(?:\r?\n){3,}/g, `${newline}${newline}`);
+
+  if (result.trim().length === 0) {
+    return '';
+  }
+
+  // Preserve leading whitespace, only trim trailing
+  return result.trimEnd() + newline;
📝 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
export function removeMarkerBlock(
content: string,
startMarker: string,
endMarker: string
): string {
const startIndex = findMarkerIndex(content, startMarker);
const endIndex = startIndex !== -1
? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
: findMarkerIndex(content, endMarker);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
return content;
}
// Find the start of the line containing the start marker
let lineStart = startIndex;
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
lineStart--;
}
// Find the end of the line containing the end marker
let lineEnd = endIndex + endMarker.length;
while (lineEnd < content.length && content[lineEnd] !== '\n') {
lineEnd++;
}
// Include the trailing newline if present
if (lineEnd < content.length && content[lineEnd] === '\n') {
lineEnd++;
}
const before = content.substring(0, lineStart);
const after = content.substring(lineEnd);
// Clean up double blank lines
let result = before + after;
result = result.replace(/\n{3,}/g, '\n\n');
// Trim leading/trailing whitespace but preserve content
return result.trim() === '' ? '' : result.trim() + '\n';
}
export function removeMarkerBlock(
content: string,
startMarker: string,
endMarker: string
): string {
const startIndex = findMarkerIndex(content, startMarker);
const endIndex = startIndex !== -1
? findMarkerIndex(content, endMarker, startIndex + startMarker.length)
: findMarkerIndex(content, endMarker);
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
return content;
}
// Find the start of the line containing the start marker
let lineStart = startIndex;
while (lineStart > 0 && content[lineStart - 1] !== '\n') {
lineStart--;
}
// Find the end of the line containing the end marker
let lineEnd = endIndex + endMarker.length;
while (lineEnd < content.length && content[lineEnd] !== '\n') {
lineEnd++;
}
// Include the trailing newline if present
if (lineEnd < content.length && content[lineEnd] === '\n') {
lineEnd++;
}
const before = content.substring(0, lineStart);
const after = content.substring(lineEnd);
const newline = content.includes('\r\n') ? '\r\n' : '\n';
// Clean up double blank lines
let result = before + after;
result = result.replace(/(?:\r?\n){3,}/g, `${newline}${newline}`);
if (result.trim().length === 0) {
return '';
}
// Preserve leading whitespace, only trim trailing
return result.trimEnd() + newline;
}
🤖 Prompt for AI Agents
In `@src/utils/file-system.ts` around lines 264 - 303, The function
removeMarkerBlock currently uses trim() which removes leading whitespace and
normalizes line endings; change it to preserve leading indentation and original
newline style by replacing trim() with trimEnd() (so leading spaces remain) and
detect the file's newline sequence (e.g., const newline =
content.includes('\r\n') ? '\r\n' : '\n') then return result.trimEnd() === '' ?
'' : result.trimEnd() + newline; update references in removeMarkerBlock and keep
findMarkerIndex usage unchanged so only trailing whitespace/newline handling is
modified.

Comment on lines +845 to +856
it('should cover all tools from the SlashCommandRegistry', () => {
const expectedTools = [
'claude', 'codebuddy', 'qoder', 'cursor', 'windsurf', 'kilocode',
'opencode', 'codex', 'github-copilot', 'amazon-q', 'factory',
'gemini', 'auggie', 'cline', 'crush', 'costrict', 'qwen',
'roocode', 'antigravity', 'iflow', 'continue',
];

for (const tool of expectedTools) {
expect(LEGACY_SLASH_COMMAND_PATHS).toHaveProperty(tool);
}
});
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that LEGACY_SLASH_COMMAND_PATHS covers all tools consistently

# Find the actual exported LEGACY_SLASH_COMMAND_PATHS keys in legacy-cleanup.ts
echo "=== Keys in LEGACY_SLASH_COMMAND_PATHS ==="
rg -A 100 'export const LEGACY_SLASH_COMMAND_PATHS' --type ts | head -100

# Check if there's a SlashCommandRegistry or similar that defines the canonical tool list
echo ""
echo "=== Looking for SlashCommandRegistry or tool definitions ==="
rg 'SlashCommandRegistry|slash.*registry' --type ts -i | head -20

Repository: Fission-AI/OpenSpec

Length of output: 7612


🏁 Script executed:

#!/bin/bash
# Find SlashCommandRegistry definition
echo "=== Looking for SlashCommandRegistry in registry.ts ==="
cat -n src/core/command-generation/registry.ts | head -200

echo ""
echo "=== Check if there's a SlashCommandRegistry type or constant ==="
rg 'type SlashCommandRegistry|const.*SlashCommandRegistry|interface.*SlashCommand' --type ts -A 5

Repository: Fission-AI/OpenSpec

Length of output: 4892


Derive expected tools from CommandAdapterRegistry instead of hardcoding.

The test hardcodes an expected tools list (21 tools) and only checks that these exist in LEGACY_SLASH_COMMAND_PATHS using toHaveProperty(). This approach is brittle because:

  1. If new adapters are registered in CommandAdapterRegistry, the test won't catch missing entries in LEGACY_SLASH_COMMAND_PATHS
  2. If tools are added to LEGACY_SLASH_COMMAND_PATHS, the test passes silently (no verification that the lists match)
  3. CommandAdapterRegistry (registered in src/core/command-generation/registry.ts) is the canonical source of supported tools, not a hardcoded list

Instead, dynamically extract tool IDs from CommandAdapterRegistry.getAll() to ensure the test automatically fails when the two sources diverge.

🤖 Prompt for AI Agents
In `@test/core/legacy-cleanup.test.ts` around lines 845 - 856, Replace the
hardcoded expectedTools array with a dynamic list derived from
CommandAdapterRegistry.getAll(): call CommandAdapterRegistry.getAll(), map to
each adapter's id (or adapter.id) to build expected IDs, then assert parity with
LEGACY_SLASH_COMMAND_PATHS by comparing keys (e.g., ensure every id exists in
LEGACY_SLASH_COMMAND_PATHS and lengths match or assert exact set equality).
Refer to CommandAdapterRegistry.getAll() and LEGACY_SLASH_COMMAND_PATHS (and
adapter.id) when locating the code to update.

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.

2 participants