Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,6 @@ opencode.json

# Codex
.codex/

# Bob
.bob/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Now tell your AI: `/opsx:propose <what-you-want-to-build>`
If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.

> [!NOTE]
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
> Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
>
> Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).

Expand Down
3 changes: 2 additions & 1 deletion docs/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ You can enable expanded workflows (`new`, `continue`, `ff`, `verify`, `sync`, `b
| Amazon Q Developer (`amazon-q`) | `.amazonq/skills/openspec-*/SKILL.md` | `.amazonq/prompts/opsx-<id>.md` |
| Antigravity (`antigravity`) | `.agent/skills/openspec-*/SKILL.md` | `.agent/workflows/opsx-<id>.md` |
| Auggie (`auggie`) | `.augment/skills/openspec-*/SKILL.md` | `.augment/commands/opsx-<id>.md` |
| IBM Bob Shell (`bob`) | `.bob/skills/openspec-*/SKILL.md` | `.bob/commands/opsx-<id>.md` |
| Claude Code (`claude`) | `.claude/skills/openspec-*/SKILL.md` | `.claude/commands/opsx/<id>.md` |
| Cline (`cline`) | `.cline/skills/openspec-*/SKILL.md` | `.clinerules/workflows/opsx-<id>.md` |
| CodeBuddy (`codebuddy`) | `.codebuddy/skills/openspec-*/SKILL.md` | `.codebuddy/commands/opsx/<id>.md` |
Expand Down Expand Up @@ -68,7 +69,7 @@ openspec init --tools none
openspec init --profile core
```

**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`
**Available tool IDs (`--tools`):** `amazon-q`, `antigravity`, `auggie`, `bob`, `claude`, `cline`, `codex`, `codebuddy`, `continue`, `costrict`, `crush`, `cursor`, `factory`, `gemini`, `github-copilot`, `iflow`, `kilocode`, `kiro`, `opencode`, `pi`, `qoder`, `qwen`, `roocode`, `trae`, `windsurf`

## Workflow-Dependent Installation

Expand Down
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions src/core/command-generation/adapters/bob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Bob Shell Command Adapter
*
* Formats commands for Bob Shell following its markdown specification.
* Commands are stored in .bob/commands/ directory.
*/

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

/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters.
*/
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}"`;
Comment on lines +15 to +21
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

Escape \r as well when serializing YAML description.

escapeYamlValue flags carriage returns at Line 17, but Line 20 doesn’t escape them. That can produce malformed or parser-dependent frontmatter on CRLF input.

🔧 Suggested fix
-    const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
+    const escaped = value
+      .replace(/\\/g, '\\\\')
+      .replace(/"/g, '\\"')
+      .replace(/\r/g, '\\r')
+      .replace(/\n/g, '\\n');
📝 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}"`;
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(/\r/g, '\\r')
.replace(/\n/g, '\\n');
return `"${escaped}"`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/command-generation/adapters/bob.ts` around lines 15 - 21, The
escapeYamlValue function currently replaces backslashes, double quotes, and
newlines but omits carriage returns; update escapeYamlValue so it also escapes
'\r' when serializing YAML (e.g., add a replacement for /\r/g to produce '\\r'
in the same escape chain used for value.replace(...)). Ensure the function still
wraps the escaped string in double quotes and preserves the existing escapes for
backslashes, quotes, and newlines.

}
return value;
}

/**
* Bob Shell adapter for command generation.
* File path: .bob/commands/opsx-<id>.md
* Frontmatter: description (optional)
*/
export const bobAdapter: ToolCommandAdapter = {
toolId: 'bob',

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

formatFile(content: CommandContent): string {
// Bob Shell supports optional YAML frontmatter with description field
return `---
description: ${escapeYamlValue(content.description)}
---

${content.body}
`;
},
};
1 change: 1 addition & 0 deletions src/core/command-generation/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export { amazonQAdapter } from './amazon-q.js';
export { antigravityAdapter } from './antigravity.js';
export { auggieAdapter } from './auggie.js';
export { bobAdapter } from './bob.js';
export { claudeAdapter } from './claude.js';
export { clineAdapter } from './cline.js';
export { codexAdapter } from './codex.js';
Expand Down
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ToolCommandAdapter } from './types.js';
import { amazonQAdapter } from './adapters/amazon-q.js';
import { antigravityAdapter } from './adapters/antigravity.js';
import { auggieAdapter } from './adapters/auggie.js';
import { bobAdapter } from './adapters/bob.js';
import { claudeAdapter } from './adapters/claude.js';
import { clineAdapter } from './adapters/cline.js';
import { codexAdapter } from './adapters/codex.js';
Expand Down Expand Up @@ -41,6 +42,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(amazonQAdapter);
CommandAdapterRegistry.register(antigravityAdapter);
CommandAdapterRegistry.register(auggieAdapter);
CommandAdapterRegistry.register(bobAdapter);
CommandAdapterRegistry.register(claudeAdapter);
CommandAdapterRegistry.register(clineAdapter);
CommandAdapterRegistry.register(codexAdapter);
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AI_TOOLS: AIToolOption[] = [
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer', skillsDir: '.amazonq' },
{ name: 'Antigravity', value: 'antigravity', available: true, successLabel: 'Antigravity', skillsDir: '.agent' },
{ name: 'Auggie (Augment CLI)', value: 'auggie', available: true, successLabel: 'Auggie', skillsDir: '.augment' },
{ name: 'Bob Shell', value: 'bob', available: true, successLabel: 'Bob Shell', skillsDir: '.bob' },
{ name: 'Claude Code', value: 'claude', available: true, successLabel: 'Claude Code', skillsDir: '.claude' },
{ name: 'Cline', value: 'cline', available: true, successLabel: 'Cline', skillsDir: '.cline' },
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex', skillsDir: '.codex' },
Expand Down
55 changes: 54 additions & 1 deletion test/core/command-generation/adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path';
import { amazonQAdapter } from '../../../src/core/command-generation/adapters/amazon-q.js';
import { antigravityAdapter } from '../../../src/core/command-generation/adapters/antigravity.js';
import { auggieAdapter } from '../../../src/core/command-generation/adapters/auggie.js';
import { bobAdapter } from '../../../src/core/command-generation/adapters/bob.js';
import { claudeAdapter } from '../../../src/core/command-generation/adapters/claude.js';
import { clineAdapter } from '../../../src/core/command-generation/adapters/cline.js';
import { codexAdapter } from '../../../src/core/command-generation/adapters/codex.js';
Expand Down Expand Up @@ -183,6 +184,58 @@ describe('command-generation/adapters', () => {
});
});


describe('bobAdapter', () => {
it('should have correct toolId', () => {
expect(bobAdapter.toolId).toBe('bob');
});

it('should generate correct file path', () => {
const filePath = bobAdapter.getFilePath('explore');
expect(filePath).toBe(path.join('.bob', 'commands', 'opsx-explore.md'));
});

it('should generate correct file paths for different commands', () => {
expect(bobAdapter.getFilePath('new')).toBe(path.join('.bob', 'commands', 'opsx-new.md'));
expect(bobAdapter.getFilePath('bulk-archive')).toBe(path.join('.bob', 'commands', 'opsx-bulk-archive.md'));
});

it('should format file with description frontmatter', () => {
const output = bobAdapter.formatFile(sampleContent);
expect(output).toContain('---\n');
expect(output).toContain('description: Enter explore mode for thinking');
expect(output).toContain('---\n\n');
expect(output).toContain('This is the command body.\n\nWith multiple lines.');
});

it('should escape YAML special characters in description', () => {
const contentWithSpecialChars: CommandContent = {
...sampleContent,
description: 'Fix: regression in "auth" feature',
};
const output = bobAdapter.formatFile(contentWithSpecialChars);
expect(output).toContain('description: "Fix: regression in \\"auth\\" feature"');
});

it('should escape newlines in description', () => {
const contentWithNewline: CommandContent = {
...sampleContent,
description: 'Line 1\nLine 2',
};
const output = bobAdapter.formatFile(contentWithNewline);
expect(output).toContain('description: "Line 1\\nLine 2"');
});

it('should handle empty description', () => {
const contentEmptyDesc: CommandContent = {
...sampleContent,
description: '',
};
const output = bobAdapter.formatFile(contentEmptyDesc);
expect(output).toContain('description: \n');
});
});

describe('clineAdapter', () => {
it('should have correct toolId', () => {
expect(clineAdapter.toolId).toBe('cline');
Expand Down Expand Up @@ -606,7 +659,7 @@ describe('command-generation/adapters', () => {
it('All adapters use path.join for paths', () => {
// Verify all adapters produce valid paths
const adapters = [
amazonQAdapter, antigravityAdapter, auggieAdapter, clineAdapter,
amazonQAdapter, antigravityAdapter, auggieAdapter, bobAdapter, clineAdapter,
codexAdapter, codebuddyAdapter, continueAdapter, costrictAdapter,
crushAdapter, factoryAdapter, geminiAdapter, githubCopilotAdapter,
iflowAdapter, kilocodeAdapter, opencodeAdapter, piAdapter, qoderAdapter,
Expand Down