Skip to content

Commit c4b6be4

Browse files
authored
feat: add CoStrict AI assistant support (#240)
* feat(ai-tools): add Costrict integration support Add support for Costrict AI tool with slash commands and configuration template. Includes configurator, registry entries, templates, and comprehensive test coverage. * feat(config): update branding from 'Costrict' to 'CoStrict'
1 parent a665807 commit c4b6be4

File tree

10 files changed

+342
-0
lines changed

10 files changed

+342
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
9292
|------|----------|
9393
| **Claude Code** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` |
9494
| **CodeBuddy Code (CLI)** | `/openspec:proposal`, `/openspec:apply`, `/openspec:archive` (`.codebuddy/commands/`) — see [docs](https://www.codebuddy.ai/cli) |
95+
| **CoStrict** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.cospec/openspec/commands/`) — see [docs](https://costrict.ai)|
9596
| **Cursor** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` |
9697
| **Cline** | Rules in `.clinerules/` directory (`.clinerules/openspec-*.md`) |
9798
| **Crush** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.crush/commands/openspec/`) |

src/core/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const AI_TOOLS: AIToolOption[] = [
2121
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code' },
2222
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline' },
2323
{ name: 'CodeBuddy Code (CLI)', value: 'codebuddy', available: true, successLabel: 'CodeBuddy Code' },
24+
{ name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict' },
2425
{ name: 'Crush', value: 'crush', available: true, successLabel: 'Crush' },
2526
{ name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor' },
2627
{ name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid' },

src/core/configurators/costrict.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import path from 'path';
2+
import { ToolConfigurator } from './base.js';
3+
import { FileSystemUtils } from '../../utils/file-system.js';
4+
import { TemplateManager } from '../templates/index.js';
5+
import { OPENSPEC_MARKERS } from '../config.js';
6+
7+
export class CostrictConfigurator implements ToolConfigurator {
8+
name = 'CoStrict';
9+
configFileName = 'COSTRICT.md';
10+
isAvailable = true;
11+
12+
async configure(projectPath: string, openspecDir: string): Promise<void> {
13+
const filePath = path.join(projectPath, this.configFileName);
14+
const content = TemplateManager.getCostrictTemplate();
15+
16+
await FileSystemUtils.updateFileWithMarkers(
17+
filePath,
18+
content,
19+
OPENSPEC_MARKERS.start,
20+
OPENSPEC_MARKERS.end
21+
);
22+
}
23+
}

src/core/configurators/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ToolConfigurator } from './base.js';
22
import { ClaudeConfigurator } from './claude.js';
33
import { ClineConfigurator } from './cline.js';
44
import { CodeBuddyConfigurator } from './codebuddy.js';
5+
import { CostrictConfigurator } from './costrict.js';
56
import { AgentsStandardConfigurator } from './agents.js';
67

78
export class ToolRegistry {
@@ -11,11 +12,13 @@ export class ToolRegistry {
1112
const claudeConfigurator = new ClaudeConfigurator();
1213
const clineConfigurator = new ClineConfigurator();
1314
const codeBuddyConfigurator = new CodeBuddyConfigurator();
15+
const costrictConfigurator = new CostrictConfigurator();
1416
const agentsConfigurator = new AgentsStandardConfigurator();
1517
// Register with the ID that matches the checkbox value
1618
this.tools.set('claude', claudeConfigurator);
1719
this.tools.set('cline', clineConfigurator);
1820
this.tools.set('codebuddy', codeBuddyConfigurator);
21+
this.tools.set('costrict', costrictConfigurator);
1922
this.tools.set('agents', agentsConfigurator);
2023
}
2124

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SlashCommandConfigurator } from './base.js';
2+
import { SlashCommandId } from '../../templates/index.js';
3+
4+
const FILE_PATHS = {
5+
proposal: '.cospec/openspec/commands/openspec-proposal.md',
6+
apply: '.cospec/openspec/commands/openspec-apply.md',
7+
archive: '.cospec/openspec/commands/openspec-archive.md',
8+
} as const satisfies Record<SlashCommandId, string>;
9+
10+
const FRONTMATTER = {
11+
proposal: `---
12+
description: "Scaffold a new OpenSpec change and validate strictly."
13+
argument-hint: feature description or request
14+
---`,
15+
apply: `---
16+
description: "Implement an approved OpenSpec change and keep tasks in sync."
17+
argument-hint: change-id
18+
---`,
19+
archive: `---
20+
description: "Archive a deployed OpenSpec change and update specs."
21+
argument-hint: change-id
22+
---`
23+
} as const satisfies Record<SlashCommandId, string>;
24+
25+
export class CostrictSlashCommandConfigurator extends SlashCommandConfigurator {
26+
readonly toolId = 'costrict';
27+
readonly isAvailable = true;
28+
29+
protected getRelativePath(id: SlashCommandId): string {
30+
return FILE_PATHS[id];
31+
}
32+
33+
protected getFrontmatter(id: SlashCommandId): string | undefined {
34+
return FRONTMATTER[id];
35+
}
36+
}

src/core/configurators/slash/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FactorySlashCommandConfigurator } from './factory.js';
1212
import { AuggieSlashCommandConfigurator } from './auggie.js';
1313
import { ClineSlashCommandConfigurator } from './cline.js';
1414
import { CrushSlashCommandConfigurator } from './crush.js';
15+
import { CostrictSlashCommandConfigurator } from './costrict.js';
1516

1617
export class SlashCommandRegistry {
1718
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
@@ -30,6 +31,7 @@ export class SlashCommandRegistry {
3031
const auggie = new AuggieSlashCommandConfigurator();
3132
const cline = new ClineSlashCommandConfigurator();
3233
const crush = new CrushSlashCommandConfigurator();
34+
const costrict = new CostrictSlashCommandConfigurator();
3335

3436
this.configurators.set(claude.toolId, claude);
3537
this.configurators.set(codeBuddy.toolId, codeBuddy);
@@ -44,6 +46,7 @@ export class SlashCommandRegistry {
4446
this.configurators.set(auggie.toolId, auggie);
4547
this.configurators.set(cline.toolId, cline);
4648
this.configurators.set(crush.toolId, crush);
49+
this.configurators.set(costrict.toolId, costrict);
4750
}
4851

4952
static register(configurator: SlashCommandConfigurator): void {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { agentsRootStubTemplate as costrictTemplate } from './agents-root-stub.js';

src/core/templates/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { agentsTemplate } from './agents-template.js';
22
import { projectTemplate, ProjectContext } from './project-template.js';
33
import { claudeTemplate } from './claude-template.js';
44
import { clineTemplate } from './cline-template.js';
5+
import { costrictTemplate } from './costrict-template.js';
56
import { agentsRootStubTemplate } from './agents-root-stub.js';
67
import { getSlashCommandBody, SlashCommandId } from './slash-command-templates.js';
78

@@ -32,6 +33,10 @@ export class TemplateManager {
3233
return clineTemplate;
3334
}
3435

36+
static getCostrictTemplate(): string {
37+
return costrictTemplate;
38+
}
39+
3540
static getAgentsStandardTemplate(): string {
3641
return agentsRootStubTemplate;
3742
}

test/core/init.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,93 @@ describe('InitCommand', () => {
995995
);
996996
expect(crushChoice.configured).toBe(true);
997997
});
998+
999+
it('should create CoStrict slash command files with templates', async () => {
1000+
queueSelections('costrict', DONE);
1001+
1002+
await initCommand.execute(testDir);
1003+
1004+
const costrictProposal = path.join(
1005+
testDir,
1006+
'.cospec/openspec/commands/openspec-proposal.md'
1007+
);
1008+
const costrictApply = path.join(
1009+
testDir,
1010+
'.cospec/openspec/commands/openspec-apply.md'
1011+
);
1012+
const costrictArchive = path.join(
1013+
testDir,
1014+
'.cospec/openspec/commands/openspec-archive.md'
1015+
);
1016+
1017+
expect(await fileExists(costrictProposal)).toBe(true);
1018+
expect(await fileExists(costrictApply)).toBe(true);
1019+
expect(await fileExists(costrictArchive)).toBe(true);
1020+
1021+
const proposalContent = await fs.readFile(costrictProposal, 'utf-8');
1022+
expect(proposalContent).toContain('---');
1023+
expect(proposalContent).toContain('description: "Scaffold a new OpenSpec change and validate strictly."');
1024+
expect(proposalContent).toContain('argument-hint: feature description or request');
1025+
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
1026+
expect(proposalContent).toContain('**Guardrails**');
1027+
1028+
const applyContent = await fs.readFile(costrictApply, 'utf-8');
1029+
expect(applyContent).toContain('---');
1030+
expect(applyContent).toContain('description: "Implement an approved OpenSpec change and keep tasks in sync."');
1031+
expect(applyContent).toContain('argument-hint: change-id');
1032+
expect(applyContent).toContain('Work through tasks sequentially');
1033+
1034+
const archiveContent = await fs.readFile(costrictArchive, 'utf-8');
1035+
expect(archiveContent).toContain('---');
1036+
expect(archiveContent).toContain('description: "Archive a deployed OpenSpec change and update specs."');
1037+
expect(archiveContent).toContain('argument-hint: change-id');
1038+
expect(archiveContent).toContain('openspec archive <id> --yes');
1039+
});
1040+
1041+
it('should mark CoStrict as already configured during extend mode', async () => {
1042+
queueSelections('costrict', DONE, 'costrict', DONE);
1043+
await initCommand.execute(testDir);
1044+
await initCommand.execute(testDir);
1045+
1046+
const secondRunArgs = mockPrompt.mock.calls[1][0];
1047+
const costrictChoice = secondRunArgs.choices.find(
1048+
(choice: any) => choice.value === 'costrict'
1049+
);
1050+
expect(costrictChoice.configured).toBe(true);
1051+
});
1052+
1053+
it('should create COSTRICT.md when CoStrict is selected', async () => {
1054+
queueSelections('costrict', DONE);
1055+
1056+
await initCommand.execute(testDir);
1057+
1058+
const costrictPath = path.join(testDir, 'COSTRICT.md');
1059+
expect(await fileExists(costrictPath)).toBe(true);
1060+
1061+
const content = await fs.readFile(costrictPath, 'utf-8');
1062+
expect(content).toContain('<!-- OPENSPEC:START -->');
1063+
expect(content).toContain("@/openspec/AGENTS.md");
1064+
expect(content).toContain('openspec update');
1065+
expect(content).toContain('<!-- OPENSPEC:END -->');
1066+
});
1067+
1068+
it('should update existing COSTRICT.md with markers', async () => {
1069+
queueSelections('costrict', DONE);
1070+
1071+
const costrictPath = path.join(testDir, 'COSTRICT.md');
1072+
const existingContent =
1073+
'# My CoStrict Instructions\nCustom instructions here';
1074+
await fs.writeFile(costrictPath, existingContent);
1075+
1076+
await initCommand.execute(testDir);
1077+
1078+
const updatedContent = await fs.readFile(costrictPath, 'utf-8');
1079+
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
1080+
expect(updatedContent).toContain("@/openspec/AGENTS.md");
1081+
expect(updatedContent).toContain('openspec update');
1082+
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
1083+
expect(updatedContent).toContain('Custom instructions here');
1084+
});
9981085
});
9991086

10001087
describe('non-interactive mode', () => {

0 commit comments

Comments
 (0)