|
1 | 1 | # AGENTS.md - Coding Agent Guidelines for Systematic |
2 | 2 |
|
3 | | -**Generated:** 2026-01-28 | **Commit:** d4bfa75 | **Branch:** main |
| 3 | +**Generated:** 2026-02-02 | **Commit:** decbf40 | **Branch:** main |
4 | 4 |
|
5 | | -## Project Overview |
| 5 | +## Overview |
6 | 6 |
|
7 | | -OpenCode plugin providing systematic engineering workflows. Converts/adapts Claude Code agents, skills, and commands from Compound Engineering Plugin (CEP) to OpenCode. |
| 7 | +OpenCode plugin providing systematic engineering workflows. Converts Claude Code (CEP) agents, skills, and commands to OpenCode format. |
8 | 8 |
|
9 | | -**Key insight:** This repo has two distinct parts: |
10 | | -1. **TypeScript source** (`src/`) - Plugin logic, tools, config handling |
11 | | -2. **Bundled assets** (`skills/`, `agents/`, `commands/`) - OpenCode Markdown content shipped with npm package |
| 9 | +**Two distinct parts:** |
| 10 | +1. **TypeScript source** (`src/`) — Plugin logic, tools, config handling |
| 11 | +2. **Bundled assets** (`skills/`, `agents/`, `commands/`) — Markdown content shipped with npm package |
12 | 12 |
|
13 | | -## Build & Test Commands |
| 13 | +## Commands |
14 | 14 |
|
15 | 15 | ```bash |
16 | | -# Install dependencies |
17 | | -bun install |
18 | | - |
19 | | -# Build (outputs to dist/) |
20 | | -bun run build |
21 | | - |
22 | | -# Type checking (strict mode) |
23 | | -bun run typecheck |
24 | | - |
25 | | -# Lint with Biome |
26 | | -bun run lint |
27 | | - |
28 | | -# Run unit tests |
29 | | -bun test tests/unit |
| 16 | +bun install # Install deps |
| 17 | +bun run build # Build to dist/ |
| 18 | +bun run typecheck # Type check (strict) |
| 19 | +bun run lint # Biome linter |
| 20 | +bun test tests/unit # Unit tests |
| 21 | +bun test tests/integration # Integration tests |
| 22 | +bun test # All tests |
| 23 | +bun test --filter "pattern" # Filter tests |
| 24 | +``` |
30 | 25 |
|
31 | | -# Run a single test file |
32 | | -bun test tests/unit/skills-core.test.ts |
| 26 | +## Stack |
33 | 27 |
|
34 | | -# Run tests matching a pattern |
35 | | -bun test --filter "extractFrontmatter" |
| 28 | +- **Runtime:** Bun (Node.js API compatible) |
| 29 | +- **Language:** TypeScript 5.7+ strict mode |
| 30 | +- **Modules:** ESM (`"type": "module"`) |
| 31 | +- **Linter:** Biome (not ESLint/Prettier) |
| 32 | +- **Tests:** `bun:test` |
36 | 33 |
|
37 | | -# Run all tests (unit + integration) |
38 | | -bun test |
| 34 | +## Structure |
39 | 35 |
|
40 | | -# Run integration tests only |
41 | | -bun test tests/integration |
| 36 | +``` |
| 37 | +systematic/ |
| 38 | +├── src/ |
| 39 | +│ ├── index.ts # Plugin entry (SystematicPlugin) |
| 40 | +│ ├── cli.ts # CLI entry |
| 41 | +│ └── lib/ # Core implementation (see src/lib/AGENTS.md) |
| 42 | +├── skills/ # 8 bundled skills (SKILL.md format) |
| 43 | +├── agents/ # 11 bundled agents (4 categories) |
| 44 | +├── commands/ # 9 bundled commands |
| 45 | +├── tests/ |
| 46 | +│ ├── unit/ # 9 test files |
| 47 | +│ └── integration/ # 2 test files |
| 48 | +└── dist/ # Build output |
42 | 49 | ``` |
43 | 50 |
|
44 | | -## Technology Stack |
45 | | - |
46 | | -- **Runtime**: Bun (not Node.js for execution, but Node.js API compatible) |
47 | | -- **Language**: TypeScript 5.7+ with strict mode |
48 | | -- **Module System**: ESM (`"type": "module"`) |
49 | | -- **Target**: ES2022 |
50 | | -- **Linter/Formatter**: Biome (not ESLint/Prettier) |
51 | | -- **Testing**: Bun's native test runner (`bun:test`) |
52 | | - |
53 | | -## Code Style |
| 51 | +## Where to Look |
| 52 | + |
| 53 | +| Task | Location | |
| 54 | +|------|----------| |
| 55 | +| Plugin hooks (config, tool, system.transform) | `src/index.ts` | |
| 56 | +| Config merging logic | `src/lib/config-handler.ts` | |
| 57 | +| Skill tool implementation | `src/lib/skill-tool.ts` | |
| 58 | +| Bootstrap injection | `src/lib/bootstrap.ts` | |
| 59 | +| CEP conversion | `src/lib/converter.ts` | |
| 60 | +| Asset discovery | `src/lib/skills.ts`, `agents.ts`, `commands.ts` | |
| 61 | +| Add new skill | `skills/<name>/SKILL.md` | |
| 62 | +| Add new agent | `agents/<category>/<name>.md` | |
| 63 | +| Add new command | `commands/<name>.md` | |
| 64 | + |
| 65 | +## Code Map |
| 66 | + |
| 67 | +| Symbol | Type | Location | Role | |
| 68 | +|--------|------|----------|------| |
| 69 | +| `SystematicPlugin` | export | src/index.ts:30 | Main plugin factory | |
| 70 | +| `createConfigHandler` | fn | src/lib/config-handler.ts:182 | Config hook impl | |
| 71 | +| `createSkillTool` | fn | src/lib/skill-tool.ts:35 | systematic_skill tool | |
| 72 | +| `getBootstrapContent` | fn | src/lib/bootstrap.ts:32 | System prompt injection | |
| 73 | +| `convertContent` | fn | src/lib/converter.ts:234 | CEP→OpenCode conversion | |
| 74 | +| `findSkillsInDir` | fn | src/lib/skills.ts:90 | Skill discovery | |
| 75 | +| `loadConfig` | fn | src/lib/config.ts:47 | JSONC config loading | |
| 76 | + |
| 77 | +## Conventions |
54 | 78 |
|
55 | 79 | ### Formatting (Biome) |
56 | | - |
57 | | -- **Indent**: 2 spaces |
58 | | -- **Quotes**: Single quotes for strings |
59 | | -- **Semicolons**: As needed (omit where possible) |
60 | | -- **Line width**: Default (no strict limit) |
| 80 | +- 2 spaces, single quotes, semicolons as-needed |
61 | 81 |
|
62 | 82 | ### Imports |
63 | | - |
64 | 83 | ```typescript |
65 | | -// 1. Node.js built-ins with node: protocol |
66 | | -import fs from 'node:fs' |
67 | | -import path from 'node:path' |
68 | | -import os from 'node:os' |
69 | | - |
70 | | -// 2. External dependencies |
71 | | -import type { Plugin } from '@opencode-ai/plugin' |
72 | | -import { tool } from '@opencode-ai/plugin/tool' |
73 | | - |
74 | | -// 3. Internal modules with .js extension (ESM requirement) |
75 | | -import { loadConfig } from './lib/config.js' |
76 | | -import * as skillsCore from './lib/skills-core.js' |
| 84 | +import fs from 'node:fs' // Node built-ins with node: protocol |
| 85 | +import type { Plugin } from '@opencode-ai/plugin' // External deps |
| 86 | +import { loadConfig } from './lib/config.js' // Internal with .js extension |
77 | 87 | ``` |
78 | 88 |
|
79 | | -### TypeScript Patterns |
80 | | - |
81 | | -```typescript |
82 | | -// Prefer function declarations over classes |
83 | | -export function extractFrontmatter(filePath: string): SkillFrontmatter { |
84 | | - // implementation |
85 | | -} |
86 | | - |
87 | | -// Use explicit return types |
88 | | -export function findSkillsInDir( |
89 | | - dir: string, |
90 | | - sourceType: 'project' | 'user' | 'bundled', |
91 | | - maxDepth = 3 |
92 | | -): SkillInfo[] { |
93 | | - // implementation |
94 | | -} |
95 | | - |
96 | | -// Define interfaces for data structures |
97 | | -export interface SkillInfo { |
98 | | - path: string |
99 | | - skillFile: string |
100 | | - name: string |
101 | | - description: string |
102 | | - sourceType: 'project' | 'user' | 'bundled' |
103 | | -} |
104 | | - |
105 | | -// Use union types for constrained values |
106 | | -type SourceType = 'project' | 'user' | 'bundled' |
107 | | - |
108 | | -// Prefer const for immutable bindings |
109 | | -const packageRoot = path.resolve(__dirname, '..') |
110 | | - |
111 | | -// Arrow functions for inline callbacks |
112 | | -const filtered = skills.filter((s) => !disabled.includes(s.name)) |
113 | | -``` |
| 89 | +### TypeScript |
| 90 | +- Function declarations over classes |
| 91 | +- Explicit return types |
| 92 | +- Interfaces for data structures |
| 93 | +- Union types for constrained values |
114 | 94 |
|
115 | 95 | ### Error Handling |
| 96 | +- Return null/empty for non-critical failures |
| 97 | +- Early return for guard clauses |
| 98 | +- Throw with context for critical errors |
116 | 99 |
|
117 | | -```typescript |
118 | | -// Return null/empty for non-critical failures |
119 | | -export function extractFrontmatter(filePath: string): SkillFrontmatter { |
120 | | - try { |
121 | | - const content = fs.readFileSync(filePath, 'utf8') |
122 | | - // parse... |
123 | | - return { name, description } |
124 | | - } catch { |
125 | | - return { name: '', description: '' } |
126 | | - } |
127 | | -} |
| 100 | +### Naming |
| 101 | +- Files: kebab-case |
| 102 | +- Functions: camelCase |
| 103 | +- Types/Interfaces: PascalCase |
| 104 | +- Tests: `*.test.ts` |
128 | 105 |
|
129 | | -// Early return for guard clauses |
130 | | -if (!fs.existsSync(dir)) return skills |
| 106 | +## Anti-Patterns |
131 | 107 |
|
132 | | -// Throw for critical errors with context |
133 | | -if (!validTypes.includes(typeArg)) { |
134 | | - console.error(`Invalid type: ${typeArg}. Must be one of: ${validTypes.join(', ')}`) |
135 | | - process.exit(1) |
136 | | -} |
137 | | -``` |
| 108 | +- `require()` — use ESM imports |
| 109 | +- Omitting `.js` extension in relative imports |
| 110 | +- Classes when functions suffice |
| 111 | +- `any` — use `unknown` with type guards |
| 112 | +- `@ts-ignore` or `@ts-expect-error` |
| 113 | +- Non-null assertions (`!`) — Biome warns |
138 | 114 |
|
139 | | -### Naming Conventions |
| 115 | +## Plugin Architecture |
140 | 116 |
|
141 | | -- **Files**: kebab-case (`skills-core.ts`, `config.ts`) |
142 | | -- **Functions**: camelCase (`findSkillsInDir`, `loadConfig`) |
143 | | -- **Interfaces/Types**: PascalCase (`SkillInfo`, `SystematicConfig`) |
144 | | -- **Constants**: SCREAMING_SNAKE_CASE or camelCase based on scope |
145 | | -- **Test files**: `*.test.ts` in `tests/` directory |
146 | | - |
147 | | -## Testing Patterns |
148 | | - |
149 | | -```typescript |
150 | | -import { afterEach, beforeEach, describe, expect, test } from 'bun:test' |
151 | | -import fs from 'node:fs' |
152 | | -import os from 'node:os' |
153 | | -import path from 'node:path' |
154 | | - |
155 | | -describe('module-name', () => { |
156 | | - let testDir: string |
157 | | - |
158 | | - beforeEach(() => { |
159 | | - testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'systematic-test-')) |
160 | | - }) |
161 | | - |
162 | | - afterEach(() => { |
163 | | - fs.rmSync(testDir, { recursive: true, force: true }) |
164 | | - }) |
165 | | - |
166 | | - describe('functionName', () => { |
167 | | - test('describes expected behavior', () => { |
168 | | - const result = functionUnderTest(input) |
169 | | - expect(result).toBe(expected) |
170 | | - }) |
171 | | - |
172 | | - test('handles edge case', () => { |
173 | | - expect(functionUnderTest(null)).toEqual([]) |
174 | | - }) |
175 | | - }) |
176 | | -}) |
177 | | -``` |
178 | | - |
179 | | -## Project Structure |
180 | | - |
181 | | -``` |
182 | | -systematic/ |
183 | | -├── src/ |
184 | | -│ ├── index.ts # Plugin entry point (SystematicPlugin) |
185 | | -│ ├── cli.ts # CLI entry point |
186 | | -│ └── lib/ |
187 | | -│ ├── agents.ts # Agent discovery + frontmatter parsing |
188 | | -│ ├── bootstrap.ts # System prompt injection |
189 | | -│ ├── commands.ts # Command discovery + frontmatter parsing |
190 | | -│ ├── config.ts # JSONC config loading (project > user) |
191 | | -│ ├── config-handler.ts # OpenCode config hook (merges bundled → existing) |
192 | | -│ ├── converter.ts # CEP to OpenCode conversion |
193 | | -│ ├── frontmatter.ts # YAML frontmatter utilities |
194 | | -│ ├── skill-tool.ts # `systematic_skill` tool implementation |
195 | | -│ ├── skills.ts # Skill discovery + frontmatter parsing |
196 | | -│ └── walk-dir.ts # Recursive directory traversal |
197 | | -├── tests/ |
198 | | -│ ├── unit/ # Unit tests (bun test tests/unit) |
199 | | -│ └── integration/ # Integration tests |
200 | | -├── skills/ # Bundled OpenCode skill definitions (SKILL.md) |
201 | | -├── agents/ # Bundled OpenCode agent definitions (Markdown) |
202 | | -├── commands/ # Bundled OpenCode command definitions (Markdown) |
203 | | -├── dist/ # Build output (git-ignored) |
204 | | -├── biome.json # Biome linter/formatter config |
205 | | -├── tsconfig.json # TypeScript config |
206 | | -└── package.json |
207 | 117 | ``` |
208 | | - |
209 | | -## Key Patterns |
210 | | - |
211 | | -### Plugin Export Pattern |
212 | | - |
213 | | -```typescript |
214 | | -export const SystematicPlugin: Plugin = async ({ client, directory }) => { |
215 | | - const config = loadConfig(directory) |
216 | | - |
217 | | - return { |
218 | | - tool: { |
219 | | - tool_name: tool({ |
220 | | - description: 'Tool description', |
221 | | - args: {}, |
222 | | - execute: async (): Promise<string> => { |
223 | | - // implementation |
224 | | - }, |
225 | | - }), |
226 | | - }, |
227 | | - } |
228 | | -} |
229 | | - |
230 | | -export default SystematicPlugin |
| 118 | +OpenCode loads plugin |
| 119 | + ↓ |
| 120 | +SystematicPlugin({ client, directory }) |
| 121 | + ↓ |
| 122 | +┌──────────────────────────────────────────┐ |
| 123 | +│ config hook: createConfigHandler() │ |
| 124 | +│ → Merges bundled agents/commands/skills│ |
| 125 | +├──────────────────────────────────────────┤ |
| 126 | +│ tool hook: systematic_skill │ |
| 127 | +│ → Loads bundled skills on demand │ |
| 128 | +├──────────────────────────────────────────┤ |
| 129 | +│ system.transform hook │ |
| 130 | +│ → Injects using-systematic bootstrap │ |
| 131 | +└──────────────────────────────────────────┘ |
231 | 132 | ``` |
232 | 133 |
|
233 | | -### Skill File Format |
234 | | - |
235 | | -Skills are directories containing `SKILL.md` with YAML frontmatter: |
| 134 | +## Skill Format |
236 | 135 |
|
237 | 136 | ```markdown |
238 | 137 | --- |
239 | 138 | name: skill-name |
240 | | -description: Use when [condition] - [what it does] |
| 139 | +description: Use when [condition] — [what it does] |
241 | 140 | --- |
242 | 141 |
|
243 | 142 | # Skill Content |
244 | 143 | ``` |
245 | 144 |
|
246 | | -### Configuration Loading |
247 | | - |
248 | | -Supports JSONC (JSON with comments). Priority: project > user > bundled. |
249 | | - |
250 | | -## Linting Rules (Biome) |
| 145 | +## Config Priority |
251 | 146 |
|
252 | | -- `noExcessiveCognitiveComplexity`: warn |
253 | | -- `noNonNullAssertion`: warn |
254 | | -- All recommended rules enabled |
255 | | -- Markdown files excluded from linting |
| 147 | +project `.opencode/systematic.json` > user `~/.config/opencode/systematic.json` > defaults |
256 | 148 |
|
257 | | -## Don'ts |
| 149 | +## Notes |
258 | 150 |
|
259 | | -- Don't use `require()` - use ESM imports |
260 | | -- Don't omit `.js` extension in relative imports |
261 | | -- Don't use classes when functions suffice |
262 | | -- Don't use `any` - prefer `unknown` with type guards |
263 | | -- Don't ignore Biome warnings without justification |
264 | | -- Don't use `@ts-ignore` or `@ts-expect-error` |
| 151 | +- Bootstrap injection is opt-out via `bootstrap.enabled: false` |
| 152 | +- Skills are registered as commands (prefixed `systematic:`) |
| 153 | +- Experimental hook: `experimental.chat.system.transform` |
0 commit comments