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
27 changes: 25 additions & 2 deletions src/lib/config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ export interface ConfigHandlerDeps {

type CommandConfig = NonNullable<Config['command']>[string]

export function toTitleCase(name: string): string {
return name
.split('-')
.map((segment) =>
segment.length > 0
? segment.charAt(0).toUpperCase() + segment.slice(1)
: segment,
)
.join('-')
}

export function formatAgentDescription(
name: string,
description: string | undefined,
): string {
const baseDescription = description || `${name} agent`
const suffix = `(${toTitleCase(name)} - Systematic)`
if (baseDescription.endsWith(suffix)) {
return baseDescription
}
return `${baseDescription} ${suffix}`
}

function loadAgentAsConfig(agentInfo: {
name: string
file: string
Expand All @@ -41,7 +64,7 @@ function loadAgentAsConfig(agentInfo: {
} = extractAgentFrontmatter(converted)

const config: AgentConfig = {
description: description || `${agentInfo.name} agent`,
description: formatAgentDescription(agentInfo.name, description),
prompt,
}

Expand Down Expand Up @@ -80,7 +103,7 @@ function loadCommandAsConfig(commandInfo: {

const config: CommandConfig = {
template: body.trim(),
description: `(systematic) ${baseDescription}`,
description: `(Systematic) ${baseDescription}`,
}

if (agent !== undefined) config.agent = agent
Expand Down
2 changes: 1 addition & 1 deletion src/lib/skill-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { parseFrontmatter } from './frontmatter.js'
import type { SkillInfo } from './skills.js'

const SKILL_PREFIX = 'systematic:'
const SKILL_DESCRIPTION_PREFIX = '(systematic - Skill) '
const SKILL_DESCRIPTION_PREFIX = '(Systematic - Skill) '

export interface LoadedSkill {
name: string
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Integration test content.`,
expect(commandNames).not.toContain('test-skill')
})

test('adds (systematic - Skill) prefix to skill descriptions', async () => {
test('adds (Systematic - Skill) prefix to skill descriptions', async () => {
const handler = createConfigHandler({
directory: testEnv.projectDir,
bundledSkillsDir: path.join(testEnv.bundledDir, 'skills'),
Expand All @@ -197,9 +197,9 @@ Integration test content.`,
await handler(config)

const skillCommand = config.command?.['systematic:test-skill']
expect(skillCommand?.description).toMatch(/^\(systematic - Skill\) /)
expect(skillCommand?.description).toMatch(/^\(Systematic - Skill\) /)
expect(skillCommand?.description).toBe(
'(systematic - Skill) A skill for integration testing',
'(Systematic - Skill) A skill for integration testing',
)
})

Expand Down
79 changes: 73 additions & 6 deletions tests/unit/config-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { Config } from '@opencode-ai/sdk'
import { createConfigHandler } from '../../src/lib/config-handler.ts'
import {
createConfigHandler,
formatAgentDescription,
toTitleCase,
} from '../../src/lib/config-handler.ts'
import { formatFrontmatter } from '../../src/lib/frontmatter.ts'

describe('config-handler', () => {
Expand Down Expand Up @@ -78,6 +82,67 @@ Command template for ${name}.`,
)
}

describe('toTitleCase', () => {
test('converts kebab-case to Title-Case', () => {
expect(toTitleCase('architecture-strategist')).toBe(
'Architecture-Strategist',
)
})

test('handles single word', () => {
expect(toTitleCase('oracle')).toBe('Oracle')
})

test('handles empty string', () => {
expect(toTitleCase('')).toBe('')
})

test('handles single-character segments', () => {
expect(toTitleCase('a-b-c')).toBe('A-B-C')
})

test('handles numbers in segments', () => {
expect(toTitleCase('v2-api-agent')).toBe('V2-Api-Agent')
})

test('preserves already-capitalized characters after first', () => {
expect(toTitleCase('REST-API')).toBe('REST-API')
expect(toTitleCase('AI-reviewer')).toBe('AI-Reviewer')
})
})

describe('formatAgentDescription', () => {
test('appends branding suffix with title-cased name', () => {
expect(
formatAgentDescription(
'code-simplicity-reviewer',
'Reviews code for simplicity',
),
).toBe(
'Reviews code for simplicity (Code-Simplicity-Reviewer - Systematic)',
)
})

test('uses fallback when description is undefined', () => {
expect(formatAgentDescription('test-agent', undefined)).toBe(
'test-agent agent (Test-Agent - Systematic)',
)
})

test('uses fallback when description is empty', () => {
expect(formatAgentDescription('test-agent', '')).toBe(
'test-agent agent (Test-Agent - Systematic)',
)
})

test('does not double-brand if suffix already present', () => {
const alreadyBranded = 'Some description (Test-Agent - Systematic)'
expect(formatAgentDescription('test-agent', alreadyBranded)).toBe(
alreadyBranded,
)
})
})

describe('createConfigHandler', () => {
test('returns a function', () => {
const handler = createConfigHandler({
Expand All @@ -104,7 +169,9 @@ Command template for ${name}.`,

expect(config.agent).toBeDefined()
expect(config.agent?.['test-agent']).toBeDefined()
expect(config.agent?.['test-agent']?.description).toBe('A test agent')
expect(config.agent?.['test-agent']?.description).toBe(
'A test agent (Test-Agent - Systematic)',
)
})

test('collects bundled commands into config', async () => {
Expand All @@ -127,7 +194,7 @@ Command template for ${name}.`,
expect(config.command).toBeDefined()
expect(config.command?.['systematic:test-command']).toBeDefined()
expect(config.command?.['systematic:test-command']?.description).toBe(
'(systematic) A test command',
'(Systematic) A test command',
)
expect(config.command?.['systematic:test-command']?.template).toContain(
'Command template for test-command',
Expand All @@ -150,7 +217,7 @@ Command template for ${name}.`,
expect(config.command).toBeDefined()
expect(config.command?.['systematic:test-skill']).toBeDefined()
expect(config.command?.['systematic:test-skill']?.description).toBe(
'(systematic - Skill) A test skill',
'(Systematic - Skill) A test skill',
)
expect(config.command?.['systematic:test-skill']?.template).toContain(
'<skill-instruction>',
Expand Down Expand Up @@ -352,7 +419,7 @@ Command template for ${name}.`,

const agent = config.agent?.['full-agent']
expect(agent).toBeDefined()
expect(agent?.description).toBe('A full agent')
expect(agent?.description).toBe('A full agent (Full-Agent - Systematic)')
expect(agent?.model).toBe('openai/gpt-4')
expect(agent?.temperature).toBe(0.7)
expect(agent?.top_p).toBe(1)
Expand Down Expand Up @@ -460,7 +527,7 @@ Full command template.`,

const command = config.command?.['systematic:full-command']
expect(command).toBeDefined()
expect(command?.description).toBe('(systematic) A full command')
expect(command?.description).toBe('(Systematic) A full command')
expect(command?.agent).toBe('oracle')
expect(command?.model).toBe('openai/gpt-4')
expect(command?.subtask).toBe(true)
Expand Down
12 changes: 6 additions & 6 deletions tests/unit/skill-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ describe('skill-loader', () => {
})

describe('formatSkillDescription', () => {
test('adds (systematic - Skill) prefix to description', () => {
test('adds (Systematic - Skill) prefix to description', () => {
expect(formatSkillDescription('A test skill', 'test')).toBe(
'(systematic - Skill) A test skill',
'(Systematic - Skill) A test skill',
)
})

test('does not double-prefix already prefixed description', () => {
expect(
formatSkillDescription('(systematic - Skill) A test skill', 'test'),
).toBe('(systematic - Skill) A test skill')
formatSkillDescription('(Systematic - Skill) A test skill', 'test'),
).toBe('(Systematic - Skill) A test skill')
})

test('uses fallback name when description is empty', () => {
expect(formatSkillDescription('', 'my-skill')).toBe(
'(systematic - Skill) my-skill skill',
'(Systematic - Skill) my-skill skill',
)
})
})
Expand Down Expand Up @@ -162,7 +162,7 @@ description: A test skill

expect(loaded.name).toBe('test-skill')
expect(loaded.prefixedName).toBe('systematic:test-skill')
expect(loaded.description).toBe('(systematic - Skill) A test skill')
expect(loaded.description).toBe('(Systematic - Skill) A test skill')
expect(loaded.wrappedTemplate).toContain('<skill-instruction>')
expect(loaded.wrappedTemplate).toContain('# Test Content')
})
Expand Down