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
41 changes: 27 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# AGENTS.md - Coding Agent Guidelines for Systematic

**Generated:** 2026-01-28 | **Commit:** d4bfa75 | **Branch:** main

## Project Overview

OpenCode plugin providing systematic engineering workflows. Converts and adapts Claude Code agents, skills, and commands from Compound Engineering Plugin (CEP) to OpenCode. Includes skills, agents, and commands for structured AI-assisted development.
OpenCode plugin providing systematic engineering workflows. Converts/adapts Claude Code agents, skills, and commands from Compound Engineering Plugin (CEP) to OpenCode.

**Key insight:** This repo has two distinct parts:
1. **TypeScript source** (`src/`) - Plugin logic, tools, config handling
2. **Bundled assets** (`skills/`, `agents/`, `commands/`) - OpenCode Markdown content shipped with npm package

## Build & Test Commands

Expand Down Expand Up @@ -175,21 +181,28 @@ describe('module-name', () => {
```
systematic/
├── src/
│ ├── index.ts # Plugin entry point
│ ├── cli.ts # CLI entry point
│ ├── index.ts # Plugin entry point (SystematicPlugin)
│ ├── cli.ts # CLI entry point
│ └── lib/
│ ├── config.ts # Configuration loading
│ ├── converter.ts # CEP to OpenCode conversion
│ └── skills-core.ts # Skill discovery/resolution
│ ├── agents.ts # Agent discovery + frontmatter parsing
│ ├── bootstrap.ts # System prompt injection
│ ├── commands.ts # Command discovery + frontmatter parsing
│ ├── config.ts # JSONC config loading (project > user)
│ ├── config-handler.ts # OpenCode config hook (merges bundled → existing)
│ ├── converter.ts # CEP to OpenCode conversion
│ ├── frontmatter.ts # YAML frontmatter utilities
│ ├── skill-tool.ts # `systematic_skill` tool implementation
│ ├── skills.ts # Skill discovery + frontmatter parsing
│ └── walk-dir.ts # Recursive directory traversal
├── tests/
│ ├── unit/ # Unit tests (bun test tests/unit)
│ └── integration/ # Integration tests
├── skills/ # Bundled skill definitions
├── agents/ # Bundled agent definitions
├── commands/ # Bundled command definitions
├── dist/ # Build output (git-ignored)
├── biome.json # Linter/formatter config
├── tsconfig.json # TypeScript config
│ ├── unit/ # Unit tests (bun test tests/unit)
│ └── integration/ # Integration tests
├── skills/ # Bundled OpenCode skill definitions (SKILL.md)
├── agents/ # Bundled OpenCode agent definitions (Markdown)
├── commands/ # Bundled OpenCode command definitions (Markdown)
├── dist/ # Build output (git-ignored)
├── biome.json # Biome linter/formatter config
├── tsconfig.json # TypeScript config
└── package.json
```

Expand Down
1 change: 0 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"includes": [
"**",
"!**/dist",
"!**/lib",
"!**/.opencode",
"!**/node_modules",
"!**/*.md"
Expand Down
4 changes: 4 additions & 0 deletions bun.lock

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@biomejs/biome": "^2.0.0",
"@opencode-ai/plugin": "^1.1.30",
"@types/bun": "latest",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.0.0",
"conventional-changelog-conventionalcommits": "^9.0.0",
"markdownlint-cli": "^0.47.0",
Expand All @@ -63,6 +64,7 @@
"typescript": "^5.7.0"
},
"dependencies": {
"js-yaml": "^4.1.1",
"jsonc-parser": "^3.3.0"
},
"publishConfig": {
Expand Down
37 changes: 13 additions & 24 deletions src/lib/agents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseFrontmatter, stripFrontmatter } from './frontmatter.js'
import { walkDir } from './walk-dir.js'
import { stripFrontmatter } from './frontmatter.js'

export interface AgentFrontmatter {
name: string
Expand Down Expand Up @@ -27,28 +27,17 @@ export function findAgentsInDir(dir: string, maxDepth = 2): AgentInfo[] {
}

export function extractAgentFrontmatter(content: string): AgentFrontmatter {
const lines = content.split('\n')

let inFrontmatter = false
let name = ''
let description = ''

for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break
inFrontmatter = true
continue
}

if (inFrontmatter) {
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/)
if (match) {
const [, key, value] = match
if (key === 'name') name = value.trim()
if (key === 'description') description = value.trim()
}
}
const { data, parseError } = parseFrontmatter<{
name?: string
description?: string
}>(content)

return {
name: !parseError && typeof data.name === 'string' ? data.name : '',
description:
!parseError && typeof data.description === 'string'
? data.description
: '',
prompt: stripFrontmatter(content),
}

return { name, description, prompt: stripFrontmatter(content) }
}
48 changes: 22 additions & 26 deletions src/lib/commands.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseFrontmatter } from './frontmatter.js'
import { walkDir } from './walk-dir.js'

export interface CommandFrontmatter {
Expand All @@ -20,7 +21,9 @@ export function findCommandsInDir(dir: string, maxDepth = 2): CommandInfo[] {

return entries.map((entry) => {
const baseName = entry.name.replace(/\.md$/, '')
const commandName = entry.category ? `/${entry.category}:${baseName}` : `/${baseName}`
const commandName = entry.category
? `/${entry.category}:${baseName}`
: `/${baseName}`
return {
name: commandName,
file: entry.path,
Expand All @@ -30,30 +33,23 @@ export function findCommandsInDir(dir: string, maxDepth = 2): CommandInfo[] {
}

export function extractCommandFrontmatter(content: string): CommandFrontmatter {
const lines = content.split('\n')

let inFrontmatter = false
let name = ''
let description = ''
let argumentHint = ''

for (const line of lines) {
if (line.trim() === '---') {
if (inFrontmatter) break
inFrontmatter = true
continue
}

if (inFrontmatter) {
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/)
if (match) {
const [, key, value] = match
if (key === 'name') name = value.trim()
if (key === 'description') description = value.trim()
if (key === 'argument-hint') argumentHint = value.trim().replace(/^["']|["']$/g, '')
}
}
const { data, parseError } = parseFrontmatter<{
name?: string
description?: string
'argument-hint'?: string
}>(content)

const argumentHintRaw =
!parseError && typeof data['argument-hint'] === 'string'
? data['argument-hint']
: ''

return {
name: !parseError && typeof data.name === 'string' ? data.name : '',
description:
!parseError && typeof data.description === 'string'
? data.description
: '',
argumentHint: argumentHintRaw.replace(/^["']|["']$/g, ''),
}

return { name, description, argumentHint }
}
49 changes: 28 additions & 21 deletions src/lib/config-handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { AgentConfig, Config } from '@opencode-ai/sdk'
import { extractAgentFrontmatter, findAgentsInDir } from './agents.js'
import { extractCommandFrontmatter, findCommandsInDir } from './commands.js'
import { loadConfig } from './config.js'
import { convertFileWithCache } from './converter.js'
import { stripFrontmatter } from './frontmatter.js'
import { extractAgentFrontmatter, findAgentsInDir } from './agents.js'
import { extractCommandFrontmatter, findCommandsInDir } from './commands.js'
import { type SkillInfo, findSkillsInDir } from './skills.js'
import { findSkillsInDir, type SkillInfo } from './skills.js'

export interface ConfigHandlerDeps {
directory: string
Expand All @@ -15,9 +15,11 @@ export interface ConfigHandlerDeps {

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

function loadAgentAsConfig(
agentInfo: { name: string; file: string; category?: string }
): AgentConfig | null {
function loadAgentAsConfig(agentInfo: {
name: string
file: string
category?: string
}): AgentConfig | null {
try {
const converted = convertFileWithCache(agentInfo.file, 'agent', {
source: 'bundled',
Expand All @@ -34,11 +36,15 @@ function loadAgentAsConfig(
}
}

function loadCommandAsConfig(
commandInfo: { name: string; file: string; category?: string }
): CommandConfig | null {
function loadCommandAsConfig(commandInfo: {
name: string
file: string
category?: string
}): CommandConfig | null {
try {
const converted = convertFileWithCache(commandInfo.file, 'command', { source: 'bundled' })
const converted = convertFileWithCache(commandInfo.file, 'command', {
source: 'bundled',
})
const { name, description } = extractCommandFrontmatter(converted)

const cleanName = commandInfo.name.replace(/^\//, '')
Expand All @@ -52,11 +58,11 @@ function loadCommandAsConfig(
}
}

function loadSkillAsCommand(
skillInfo: SkillInfo
): CommandConfig | null {
function loadSkillAsCommand(skillInfo: SkillInfo): CommandConfig | null {
try {
const converted = convertFileWithCache(skillInfo.skillFile, 'skill', { source: 'bundled' })
const converted = convertFileWithCache(skillInfo.skillFile, 'skill', {
source: 'bundled',
})

return {
template: stripFrontmatter(converted),
Expand All @@ -69,7 +75,7 @@ function loadSkillAsCommand(

function collectAgents(
dir: string,
disabledAgents: string[]
disabledAgents: string[],
): NonNullable<Config['agent']> {
const agents: NonNullable<Config['agent']> = {}
const agentList = findAgentsInDir(dir)
Expand All @@ -88,7 +94,7 @@ function collectAgents(

function collectCommands(
dir: string,
disabledCommands: string[]
disabledCommands: string[],
): NonNullable<Config['command']> {
const commands: NonNullable<Config['command']> = {}
const commandList = findCommandsInDir(dir)
Expand All @@ -108,7 +114,7 @@ function collectCommands(

function collectSkillsAsCommands(
dir: string,
disabledSkills: string[]
disabledSkills: string[],
): NonNullable<Config['command']> {
const commands: NonNullable<Config['command']> = {}
const skillList = findSkillsInDir(dir)
Expand All @@ -135,24 +141,25 @@ function collectSkillsAsCommands(
* Existing OpenCode config is preserved and takes precedence.
*/
export function createConfigHandler(deps: ConfigHandlerDeps) {
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } = deps
const { directory, bundledSkillsDir, bundledAgentsDir, bundledCommandsDir } =
deps

return async (config: Config): Promise<void> => {
const systematicConfig = loadConfig(directory)

const bundledAgents = collectAgents(
bundledAgentsDir,
systematicConfig.disabled_agents
systematicConfig.disabled_agents,
)

const bundledCommands = collectCommands(
bundledCommandsDir,
systematicConfig.disabled_commands
systematicConfig.disabled_commands,
)

const bundledSkills = collectSkillsAsCommands(
bundledSkillsDir,
systematicConfig.disabled_skills
systematicConfig.disabled_skills,
)

const existingAgents = config.agent ?? {}
Expand Down
Loading