Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
| **Auggie (Augment CLI)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.augment/commands/`) |
| **Qwen Code** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.qwen/commands/`) |
| **iFlow (iflow-cli)** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.iflow/commands/`) |


Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },
{ name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI' },
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow' },
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode' },
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code' },
{ name: 'Qoder (CLI)', value: 'qoder', available: true, successLabel: 'Qoder' },
Expand Down
23 changes: 23 additions & 0 deletions src/core/configurators/iflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import path from "path";
import { ToolConfigurator } from "./base.js";
import { FileSystemUtils } from "../../utils/file-system.js";
import { TemplateManager } from "../templates/index.js";
import { OPENSPEC_MARKERS } from "../config.js";

export class IflowConfigurator implements ToolConfigurator {
name = "iFlow";
configFileName = "IFLOW.md";
isAvailable = true;

async configure(projectPath: string, openspecDir: string): Promise<void> {
const filePath = path.join(projectPath, this.configFileName);
const content = TemplateManager.getClaudeTemplate();

await FileSystemUtils.updateFileWithMarkers(
filePath,
content,
OPENSPEC_MARKERS.start,
OPENSPEC_MARKERS.end
);
Comment on lines +14 to +21
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 2, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use an iFlow-specific template for IFLOW.md content.

Lines 14-21 call TemplateManager.getClaudeTemplate(), which writes Claude-specific guidance into IFLOW.md. That gives users the wrong setup steps for iFlow. Please add an iFlow template (e.g., TemplateManager.getIflowTemplate()) and use it here so the configurator emits the correct instructions.

🤖 Prompt for AI Agents
In src/core/configurators/iflow.ts around lines 14 to 21, the configurator is
using TemplateManager.getClaudeTemplate() which injects Claude-specific guidance
into IFLOW.md; replace that call with TemplateManager.getIflowTemplate() so the
file contains iFlow-specific instructions, and if getIflowTemplate doesn't exist
add it to TemplateManager (or import the correct iFlow template function)
returning the proper iFlow content; keep the subsequent
FileSystemUtils.updateFileWithMarkers call unchanged.

Choose a reason for hiding this comment

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

while ClaudeTemplate also works for iflow or other cli.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ydai-hub, thank you for clarifying! I understand now that the Claude template is generic enough to work with iFlow and other CLI tools, so using getClaudeTemplate() here is appropriate.


✏️ Learnings added
Learnt from: ydai-hub
Repo: Fission-AI/OpenSpec PR: 268
File: src/core/configurators/iflow.ts:14-21
Timestamp: 2025-11-17T12:07:21.674Z
Learning: In the OpenSpec repository, `TemplateManager.getClaudeTemplate()` returns a generic template that works for iFlow and other CLI tools, not just Claude-specific content. It's acceptable to use this template across different tool configurators.

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.

}
}
3 changes: 3 additions & 0 deletions src/core/configurators/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ClineConfigurator } from './cline.js';
import { CodeBuddyConfigurator } from './codebuddy.js';
import { CostrictConfigurator } from './costrict.js';
import { QoderConfigurator } from './qoder.js';
import { IflowConfigurator } from './iflow.js';
import { AgentsStandardConfigurator } from './agents.js';
import { QwenConfigurator } from './qwen.js';

Expand All @@ -16,6 +17,7 @@ export class ToolRegistry {
const codeBuddyConfigurator = new CodeBuddyConfigurator();
const costrictConfigurator = new CostrictConfigurator();
const qoderConfigurator = new QoderConfigurator();
const iflowConfigurator = new IflowConfigurator();
const agentsConfigurator = new AgentsStandardConfigurator();
const qwenConfigurator = new QwenConfigurator();
// Register with the ID that matches the checkbox value
Expand All @@ -24,6 +26,7 @@ export class ToolRegistry {
this.tools.set('codebuddy', codeBuddyConfigurator);
this.tools.set('costrict', costrictConfigurator);
this.tools.set('qoder', qoderConfigurator);
this.tools.set('iflow', iflowConfigurator);
this.tools.set('agents', agentsConfigurator);
this.tools.set('qwen', qwenConfigurator);
}
Expand Down
42 changes: 42 additions & 0 deletions src/core/configurators/slash/iflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SlashCommandConfigurator } from './base.js';
import { SlashCommandId } from '../../templates/index.js';

const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: '.iflow/commands/openspec-proposal.md',
apply: '.iflow/commands/openspec-apply.md',
archive: '.iflow/commands/openspec-archive.md'
};

const FRONTMATTER: Record<SlashCommandId, string> = {
proposal: `---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---`,
apply: `---
name: /openspec-apply
id: openspec-apply
category: OpenSpec
description: Implement an approved OpenSpec change and keep tasks in sync.
---`,
archive: `---
name: /openspec-archive
id: openspec-archive
category: OpenSpec
description: Archive a deployed OpenSpec change and update specs.
---`
};

export class IflowSlashCommandConfigurator extends SlashCommandConfigurator {
readonly toolId = 'iflow';
readonly isAvailable = true;

protected getRelativePath(id: SlashCommandId): string {
return FILE_PATHS[id];
}

protected getFrontmatter(id: SlashCommandId): string {
return FRONTMATTER[id];
}
}
3 changes: 3 additions & 0 deletions src/core/configurators/slash/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CostrictSlashCommandConfigurator } from './costrict.js';
import { QwenSlashCommandConfigurator } from './qwen.js';
import { RooCodeSlashCommandConfigurator } from './roocode.js';
import { AntigravitySlashCommandConfigurator } from './antigravity.js';
import { IflowSlashCommandConfigurator } from './iflow.js';

export class SlashCommandRegistry {
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
Expand All @@ -42,6 +43,7 @@ export class SlashCommandRegistry {
const qwen = new QwenSlashCommandConfigurator();
const roocode = new RooCodeSlashCommandConfigurator();
const antigravity = new AntigravitySlashCommandConfigurator();
const iflow = new IflowSlashCommandConfigurator();

this.configurators.set(claude.toolId, claude);
this.configurators.set(codeBuddy.toolId, codeBuddy);
Expand All @@ -62,6 +64,7 @@ export class SlashCommandRegistry {
this.configurators.set(qwen.toolId, qwen);
this.configurators.set(roocode.toolId, roocode);
this.configurators.set(antigravity.toolId, antigravity);
this.configurators.set(iflow.toolId, iflow);
}

static register(configurator: SlashCommandConfigurator): void {
Expand Down
53 changes: 53 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,59 @@ describe('InitCommand', () => {
expect(updatedContent).not.toContain('Custom instruction added by user');
});

it('should create IFlow CLI slash command files with templates', async () => {
queueSelections('iflow', DONE);
await initCommand.execute(testDir);

const iflowProposal = path.join(
testDir,
'.iflow/commands/openspec-proposal.md'
);
const iflowApply = path.join(
testDir,
'.iflow/commands/openspec-apply.md'
);
const iflowArchive = path.join(
testDir,
'.iflow/commands/openspec-archive.md'
);

expect(await fileExists(iflowProposal)).toBe(true);
expect(await fileExists(iflowApply)).toBe(true);
expect(await fileExists(iflowArchive)).toBe(true);

const proposalContent = await fs.readFile(iflowProposal, 'utf-8');
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
expect(proposalContent).toContain('**Guardrails**');
expect(proposalContent).toContain('<!-- OPENSPEC:END -->');

const applyContent = await fs.readFile(iflowApply, 'utf-8');
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
expect(applyContent).toContain('Work through tasks sequentially');

const archiveContent = await fs.readFile(iflowArchive, 'utf-8');
expect(archiveContent).toContain('description: Archive a deployed OpenSpec change and update specs.');
expect(archiveContent).toContain('openspec archive <id>');
});

it('should update existing IFLOW.md with markers', async () => {
queueSelections('iflow', DONE);

const iflowPath = path.join(testDir, 'IFLOW.md');
const existingContent = '# My IFLOW Instructions\nCustom instructions here';
await fs.writeFile(iflowPath, existingContent);

await initCommand.execute(testDir);

const updatedContent = await fs.readFile(iflowPath, 'utf-8');
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
expect(updatedContent).toContain("@/openspec/AGENTS.md");
expect(updatedContent).toContain('openspec update');
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
expect(updatedContent).toContain('Custom instructions here');
});

it('should create OpenCode slash command files with templates', async () => {
queueSelections('opencode', DONE);

Expand Down
47 changes: 47 additions & 0 deletions test/core/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,53 @@ Old Gemini body

consoleSpy.mockRestore();
});

it('should refresh existing IFLOW slash commands', async () => {
const iflowProposal = path.join(
testDir,
'.iflow/commands/openspec-proposal.md'
);
await fs.mkdir(path.dirname(iflowProposal), { recursive: true });
const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."

prompt = """
<!-- OPENSPEC:START -->
Old IFlow body
<!-- OPENSPEC:END -->
"""
`;
await fs.writeFile(iflowProposal, initialContent);

const consoleSpy = vi.spyOn(console, 'log');

await updateCommand.execute(testDir);

const updated = await fs.readFile(iflowProposal, 'utf-8');
expect(updated).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
expect(updated).toContain('<!-- OPENSPEC:START -->');
expect(updated).toContain('**Guardrails**');
expect(updated).toContain('<!-- OPENSPEC:END -->');
expect(updated).not.toContain('Old IFlow body');

const iflowApply = path.join(
testDir,
'.iflow/commands/openspec-apply.md'
);
const iflowArchive = path.join(
testDir,
'.iflow/commands/openspec-archive.md'
);

await expect(FileSystemUtils.fileExists(iflowApply)).resolves.toBe(false);
await expect(FileSystemUtils.fileExists(iflowArchive)).resolves.toBe(false);

const [logMessage] = consoleSpy.mock.calls[0];
expect(logMessage).toContain(
'Updated slash commands: .iflow/commands/openspec-proposal.md'
);

consoleSpy.mockRestore();
});
Comment on lines +667 to +712
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

Fix the inconsistent file format in the test.

The initialContent (lines 673-680) mixes markdown and TOML syntax. It starts with description: (markdown frontmatter style) but then includes prompt = """ (TOML style). Based on the init test (line 442 in init.test.ts), iFlow uses markdown format with a frontmatter description, not TOML.

Update the initialContent to use consistent markdown format:

-    const initialContent = `description: Scaffold a new OpenSpec change and validate strictly."
-
-prompt = """
-<!-- OPENSPEC:START -->
-Old IFlow body
-<!-- OPENSPEC:END -->
-"""
-`;
+    const initialContent = `description: Scaffold a new OpenSpec change and validate strictly.
+
+<!-- OPENSPEC:START -->
+Old IFlow body
+<!-- OPENSPEC:END -->
+`;

Also remove the TOML-style prompt assertion on line 689 if it doesn't apply to the markdown format.

🤖 Prompt for AI Agents
In test/core/update.test.ts around lines 667 to 712, the test's initialContent
mixes TOML-style prompt syntax with a markdown frontmatter description; replace
initialContent with a consistent markdown file: add a YAML/markdown frontmatter
block for description (--- description: ... ---) followed by the OPENSPEC body
(the <!-- OPENSPEC:START -->...<!-- OPENSPEC:END --> segment) so the file
matches the project's markdown command format, and remove any assertion that
expects TOML-style prompt syntax (e.g., a check for `prompt = """` or similar)
around line 689 so the test only asserts markdown-appropriate content.


it('should refresh existing Factory slash commands', async () => {
const factoryPath = path.join(
Expand Down
Loading