@fro.bot/systematic 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ # The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Marcus R. Brown <git@mrbro.dev>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,122 +1,324 @@
1
- # Systematic
1
+ <div align="center">
2
2
 
3
- An OpenCode plugin providing systematic engineering workflows from the [Compound Engineering Plugin (CEP)](https://github.com/EveryInc/compound-engineering-plugin) Claude Code plugin, adapted for OpenCode.
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="./assets/banner.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="./assets/banner.svg">
6
+ <img alt="Systematic - Structured Engineering Workflows for OpenCode" src="./assets/banner.svg" width="100%">
7
+ </picture>
4
8
 
5
- ## Installation
9
+ <br><br>
10
+
11
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/marcusrbrown/systematic/main.yaml?style=flat-square&label=build&labelColor=1a1a2e&color=4FD1C5)](https://github.com/marcusrbrown/systematic/actions)
12
+ [![npm version](https://img.shields.io/npm/v/@fro.bot/systematic?style=flat-square&label=npm&labelColor=1a1a2e&color=E91E8C)](https://www.npmjs.com/package/@fro.bot/systematic)
13
+ [![License](https://img.shields.io/badge/license-MIT-F5A623?style=flat-square&labelColor=1a1a2e)](LICENSE)
14
+
15
+ <br>
16
+
17
+ **[Overview](#overview)** · **[Quick Start](#quick-start)** · **[Skills](#skills)** · **[Agents](#agents)** · **[Commands](#commands)** · **[Development](#development)**
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Overview
24
+
25
+ Systematic is an [OpenCode](https://opencode.ai/) plugin that transforms your AI assistant into a **disciplined engineering collaborator**. It provides battle-tested workflows adapted from the [Compound Engineering Plugin (CEP)](https://github.com/EveryInc/compound-engineering-plugin) for Claude Code.
26
+
27
+ ### Why Systematic?
28
+
29
+ Most AI coding assistants respond to requests without structure or methodology. This leads to inconsistent outputs, missed requirements, and wasted iterations.
30
+
31
+ **Systematic solves this with structured workflows.** The plugin injects proven engineering processes directly into your AI's system prompt, enabling it to:
32
+
33
+ - **Brainstorm systematically** before jumping to implementation
34
+ - **Plan with rigor** using multi-phase workflows
35
+ - **Review code architecturally** with specialized agents
36
+ - **Follow consistent patterns** across your entire team
37
+
38
+ ### Key Features
39
+
40
+ - **🧠 Structured Skills** — Pre-built workflows for brainstorming, planning, and code review
41
+ - **🤖 Specialized Agents** — Purpose-built subagents for architecture, security, and performance
42
+ - **⚡ Zero Configuration** — Works immediately after installation via config hooks
43
+ - **🔧 Extensible** — Add project-specific skills and commands alongside bundled ones
44
+ - **📦 Batteries Included** — Skills, agents, and commands ship with the npm package
45
+
46
+ ## Quick Start
47
+
48
+ ### Prerequisites
49
+
50
+ - [OpenCode](https://opencode.ai/) installed and configured
51
+ - Node.js 18+ or Bun runtime
52
+
53
+ ### Installation
54
+
55
+ Install the plugin via npm:
6
56
 
7
57
  ```bash
8
58
  npm install @fro.bot/systematic
9
59
  ```
10
60
 
11
- Add to your OpenCode config (`~/.config/opencode/opencode.json`):
61
+ Add it to your OpenCode configuration (`~/.config/opencode/opencode.json`):
12
62
 
13
63
  ```json
14
64
  {
15
- "plugin": ["@fro.bot/systematic"]
65
+ "plugins": ["@fro.bot/systematic"]
16
66
  }
17
67
  ```
18
68
 
19
- ## Features
69
+ That's it. Restart OpenCode and the plugin's skills, agents, and commands are available immediately.
70
+
71
+ > [!NOTE]
72
+ > Systematic uses OpenCode's `config` hook to automatically register all bundled content. No manual file copying required.
73
+
74
+ ### Verify Installation
75
+
76
+ In any OpenCode conversation, type:
77
+
78
+ ```
79
+ /systematic:using-systematic
80
+ ```
81
+
82
+ If the skill loads and displays usage instructions, the plugin is working correctly.
20
83
 
21
- ### Skills
84
+ ## Skills
22
85
 
23
- Systematic includes battle-tested engineering workflows:
86
+ Skills are structured workflows that guide the AI through systematic engineering processes. They're loaded via the `systematic_skill` tool.
24
87
 
25
88
  | Skill | Description |
26
89
  |-------|-------------|
27
- | `using-systematic` | Bootstrap skill for discovering and using other skills |
28
- | `brainstorming` | Collaborative design workflow |
29
- | `agent-browser` | Browser automation with Playwright |
30
- | `agent-native-architecture` | Design systems for AI agents |
31
- | `compound-docs` | Create and maintain compound documentation |
32
- | `create-agent-skills` | Write new skills for AI agents |
33
- | `file-todos` | Manage TODO items in files |
34
- | `git-worktree` | Use git worktrees for isolated development |
90
+ | `using-systematic` | Bootstrap skill teaches the AI how to discover and use other skills |
91
+ | `brainstorming` | Collaborative design workflow for exploring ideas before planning |
92
+ | `agent-browser` | Browser automation using Vercel's agent-browser CLI |
93
+ | `agent-native-architecture` | Design systems where AI agents are first-class citizens |
94
+ | `compound-docs` | Capture solved problems as categorized documentation |
95
+ | `create-agent-skills` | Expert guidance for writing and refining skills |
96
+ | `file-todos` | File-based todo tracking with status and dependency management |
97
+ | `git-worktree` | Manage git worktrees for isolated parallel development |
35
98
 
36
- ### Commands
99
+ ### How Skills Work
37
100
 
38
- Quick shortcuts to invoke workflows:
101
+ Skills are Markdown files with YAML frontmatter. When loaded, their content is injected into the conversation, guiding the AI's behavior:
39
102
 
40
- **Workflows:**
103
+ ```markdown
104
+ ---
105
+ name: brainstorming
106
+ description: This skill should be used before implementing features...
107
+ ---
41
108
 
42
- - `/workflows:brainstorm` - Start collaborative brainstorming
43
- - `/workflows:compound` - Build compound documentation
44
- - `/workflows:plan` - Create implementation plans
45
- - `/workflows:review` - Run code review with agents
46
- - `/workflows:work` - Execute planned work
109
+ # Brainstorming
47
110
 
48
- **Utilities:**
111
+ This skill provides detailed process knowledge for effective brainstorming...
112
+ ```
49
113
 
50
- - `/agent-native-audit` - Audit code for agent-native patterns
51
- - `/create-agent-skill` - Create a new skill
52
- - `/deepen-plan` - Add detail to existing plans
53
- - `/lfg` - Let's go - start working immediately
114
+ The AI is instructed to invoke skills **before** taking action — even with a 1% chance a skill might apply.
54
115
 
55
- ### Agents
116
+ ## Agents
56
117
 
57
- Specialized agents organized by category:
118
+ Agents are specialized subagents with pre-configured prompts and expertise. They're registered automatically via the config hook.
58
119
 
59
- **Review:**
120
+ ### Review Agents
60
121
 
61
- - `architecture-strategist` - Architectural review
62
- - `security-sentinel` - Security review
63
- - `code-simplicity-reviewer` - Complexity review
64
- - `pattern-recognition-specialist` - Pattern analysis
65
- - `performance-oracle` - Performance review
122
+ | Agent | Purpose |
123
+ |-------|---------|
124
+ | `architecture-strategist` | Analyze code changes from an architectural perspective |
125
+ | `security-sentinel` | Security audits, vulnerability assessment, OWASP compliance |
126
+ | `code-simplicity-reviewer` | Final review pass for simplicity and YAGNI principles |
127
+ | `pattern-recognition-specialist` | Detect design patterns, anti-patterns, and code smells |
128
+ | `performance-oracle` | Performance analysis, bottleneck identification, scalability |
66
129
 
67
- **Research:**
130
+ ### Research Agents
68
131
 
69
- - `framework-docs-researcher` - Documentation research
132
+ | Agent | Purpose |
133
+ |-------|---------|
134
+ | `framework-docs-researcher` | Gather framework documentation and best practices |
70
135
 
71
- ## Config Hook
136
+ ### Using Agents
72
137
 
73
- Systematic uses OpenCode's `config` hook to automatically register bundled agents, commands, and skills directly into OpenCode's configuration. This means:
138
+ Agents are invoked via OpenCode's `@mention` syntax or `delegate_task`:
74
139
 
75
- - **Zero configuration required** - All bundled content is available immediately after installing the plugin
76
- - **No file copying** - Skills, agents, and commands ship with the npm package
77
- - **Existing config preserved** - Your OpenCode configuration settings take precedence over bundled content
140
+ ```
141
+ @architecture-strategist Review the authentication refactoring in this PR
142
+ ```
78
143
 
79
- ## Tools
144
+ Or programmatically in skills/commands:
145
+
146
+ ```
147
+ delegate_task(subagent_type="architecture-strategist", prompt="Review...")
148
+ ```
80
149
 
81
- The plugin provides these tools to OpenCode:
150
+ ## Commands
82
151
 
83
- | Tool | Description |
84
- |------|-------------|
85
- | `systematic_skill` | Load Systematic bundled skills |
152
+ Commands are slash-invokable shortcuts that trigger workflows or actions.
153
+
154
+ ### Workflow Commands
86
155
 
87
- The bootstrap skill instructs OpenCode to use the native `skill` tool to load non-Systematic skills.
156
+ | Command | Description |
157
+ |---------|-------------|
158
+ | `/workflows:brainstorm` | Explore requirements through collaborative dialogue |
159
+ | `/workflows:plan` | Create detailed implementation plans |
160
+ | `/workflows:review` | Run code review with specialized agents |
161
+ | `/workflows:work` | Execute planned work systematically |
162
+ | `/workflows:compound` | Build compound documentation |
163
+
164
+ ### Utility Commands
165
+
166
+ | Command | Description |
167
+ |---------|-------------|
168
+ | `/lfg` | "Let's go" — start working immediately |
169
+ | `/create-agent-skill` | Create a new skill with guidance |
170
+ | `/deepen-plan` | Add detail to existing plans |
171
+ | `/agent-native-audit` | Audit code for agent-native patterns |
88
172
 
89
173
  ## Configuration
90
174
 
175
+ Systematic works out of the box, but you can customize it via configuration files.
176
+
177
+ ### Plugin Configuration
178
+
91
179
  Create `~/.config/opencode/systematic.json` or `.opencode/systematic.json` to disable specific bundled content:
92
180
 
93
181
  ```json
94
182
  {
95
- "disabled_skills": [],
183
+ "disabled_skills": ["git-worktree"],
96
184
  "disabled_agents": [],
97
185
  "disabled_commands": []
98
186
  }
99
187
  ```
100
188
 
189
+ ### Project-Specific Content
190
+
191
+ Add your own skills, agents, and commands alongside bundled ones:
192
+
193
+ ```
194
+ .opencode/
195
+ ├── skills/
196
+ │ └── my-skill/
197
+ │ └── SKILL.md
198
+ ├── agents/
199
+ │ └── my-agent.md
200
+ └── commands/
201
+ └── my-command.md
202
+ ```
203
+
204
+ Project-level content takes precedence over bundled content with the same name.
205
+
206
+ ## Tools
207
+
208
+ The plugin exposes one tool to OpenCode:
209
+
210
+ | Tool | Description |
211
+ |------|-------------|
212
+ | `systematic_skill` | Load Systematic bundled skills by name |
213
+
214
+ For non-Systematic skills (project or user-level), use OpenCode's native `skill` tool.
215
+
216
+ ## How It Works
217
+
218
+ Systematic uses three OpenCode plugin hooks:
219
+
220
+ ```mermaid
221
+ flowchart TB
222
+ A[Plugin Loaded] --> B[config hook]
223
+ A --> C[tool hook]
224
+ A --> D[system.transform hook]
225
+
226
+ B --> E[Merge bundled agents/commands/skills into OpenCode config]
227
+ C --> F[Register systematic_skill tool]
228
+ D --> G[Inject bootstrap prompt into every conversation]
229
+
230
+ style A fill:#e1f5fe
231
+ style E fill:#f1f8e9
232
+ style F fill:#fff3e0
233
+ style G fill:#fce4ec
234
+ ```
235
+
236
+ 1. **`config` hook** — Merges bundled assets into your OpenCode configuration
237
+ 2. **`tool` hook** — Registers the `systematic_skill` tool for loading skills
238
+ 3. **`system.transform` hook** — Injects the "Using Systematic" guide into system prompts
239
+
240
+ This architecture ensures skills, agents, and commands are available immediately without manual setup.
241
+
101
242
  ## Development
102
243
 
244
+ ### Prerequisites
245
+
246
+ - [Bun](https://bun.sh/) runtime
247
+ - Node.js 18+ (for compatibility)
248
+
249
+ ### Setup
250
+
103
251
  ```bash
252
+ # Clone the repository
253
+ git clone https://github.com/marcusrbrown/systematic.git
254
+ cd systematic
255
+
104
256
  # Install dependencies
105
257
  bun install
106
258
 
107
- # Build
259
+ # Build the plugin
108
260
  bun run build
109
261
 
110
- # Typecheck
262
+ # Run type checking
111
263
  bun run typecheck
112
264
 
113
- # Lint
265
+ # Run linter
114
266
  bun run lint
115
267
 
116
- # Run tests
268
+ # Run unit tests
117
269
  bun test
118
270
  ```
119
271
 
272
+ ### Project Structure
273
+
274
+ ```
275
+ ├── src/
276
+ │ ├── index.ts # Plugin entry point
277
+ │ ├── cli.ts # CLI entry point
278
+ │ └── lib/
279
+ │ ├── bootstrap.ts # System prompt injection
280
+ │ ├── config.ts # JSONC config loading
281
+ │ ├── config-handler.ts # OpenCode config hook
282
+ │ ├── skill-tool.ts # systematic_skill tool
283
+ │ ├── skills.ts # Skill discovery
284
+ │ ├── agents.ts # Agent discovery
285
+ │ └── commands.ts # Command discovery
286
+ ├── skills/ # Bundled skills (SKILL.md files)
287
+ ├── agents/ # Bundled agents (Markdown)
288
+ ├── commands/ # Bundled commands (Markdown)
289
+ ├── tests/
290
+ │ ├── unit/ # Unit tests
291
+ │ └── integration/ # Integration tests
292
+ └── dist/ # Build output
293
+ ```
294
+
295
+ ### Testing
296
+
297
+ ```bash
298
+ # Run all unit tests
299
+ bun test tests/unit
300
+
301
+ # Run a specific test file
302
+ bun test tests/unit/skills.test.ts
303
+
304
+ # Run integration tests
305
+ bun test tests/integration
306
+ ```
307
+
308
+ ### Contributing
309
+
310
+ See [`AGENTS.md`](./AGENTS.md) for detailed development guidelines, code style conventions, and architecture overview.
311
+
312
+ ## Converting from Claude Code
313
+
314
+ Migrating skills, agents, or commands from Claude Code (CEP) to Systematic? See the [Conversion Guide](./docs/CONVERSION-GUIDE.md) for field mappings and examples.
315
+
316
+ ## References
317
+
318
+ - [OpenCode Documentation](https://opencode.ai/docs/) — Official OpenCode platform docs
319
+ - [Compound Engineering Plugin](https://github.com/EveryInc/compound-engineering-plugin) — Original Claude Code workflows
320
+ - [Plugin Source Code](https://github.com/marcusrbrown/systematic) — View the implementation
321
+
120
322
  ## License
121
323
 
122
- MIT
324
+ [MIT](LICENSE) © Marcus R. Brown
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  findCommandsInDir,
7
7
  findSkillsInDir,
8
8
  getConfigPaths
9
- } from "./index-kjhs9jeg.js";
9
+ } from "./index-95qwq9ph.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import fs from "fs";
@@ -96,9 +96,103 @@ function formatFrontmatter(data) {
96
96
  return ["---", yamlContent, "---"].join(`
97
97
  `);
98
98
  }
99
- function stripFrontmatter(content) {
100
- const { body, hadFrontmatter } = parseFrontmatter(content);
101
- return hadFrontmatter ? body.trim() : content.trim();
99
+
100
+ // src/lib/validation.ts
101
+ function isRecord(value) {
102
+ return typeof value === "object" && value !== null && !Array.isArray(value);
103
+ }
104
+ function isPermissionSetting(value) {
105
+ return value === "ask" || value === "allow" || value === "deny";
106
+ }
107
+ function isToolsMap(value) {
108
+ if (!isRecord(value))
109
+ return false;
110
+ return Object.values(value).every((entry) => typeof entry === "boolean");
111
+ }
112
+ function isAgentMode(value) {
113
+ return value === "subagent" || value === "primary" || value === "all";
114
+ }
115
+ function extractSimplePermission(data, key) {
116
+ if (!(key in data))
117
+ return;
118
+ const value = data[key];
119
+ return isPermissionSetting(value) ? value : null;
120
+ }
121
+ function extractBashPermission(data) {
122
+ if (!("bash" in data))
123
+ return;
124
+ const bash = data.bash;
125
+ if (isPermissionSetting(bash))
126
+ return bash;
127
+ if (isRecord(bash)) {
128
+ const entries = Object.entries(bash);
129
+ if (entries.every(([, setting]) => isPermissionSetting(setting))) {
130
+ return Object.fromEntries(entries);
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ function buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory) {
136
+ const permission = {};
137
+ if (edit)
138
+ permission.edit = edit;
139
+ if (bash)
140
+ permission.bash = bash;
141
+ if (webfetch)
142
+ permission.webfetch = webfetch;
143
+ if (doom_loop)
144
+ permission.doom_loop = doom_loop;
145
+ if (external_directory)
146
+ permission.external_directory = external_directory;
147
+ return Object.keys(permission).length > 0 ? permission : undefined;
148
+ }
149
+ function normalizePermission(value) {
150
+ if (!isRecord(value))
151
+ return;
152
+ const bash = extractBashPermission(value);
153
+ if (bash === null)
154
+ return;
155
+ const edit = extractSimplePermission(value, "edit");
156
+ if (edit === null)
157
+ return;
158
+ const webfetch = extractSimplePermission(value, "webfetch");
159
+ if (webfetch === null)
160
+ return;
161
+ const doom_loop = extractSimplePermission(value, "doom_loop");
162
+ if (doom_loop === null)
163
+ return;
164
+ const external_directory = extractSimplePermission(value, "external_directory");
165
+ if (external_directory === null)
166
+ return;
167
+ return buildPermissionObject(edit, bash, webfetch, doom_loop, external_directory);
168
+ }
169
+ function extractString(data, key, fallback = "") {
170
+ const value = data[key];
171
+ return typeof value === "string" ? value : fallback;
172
+ }
173
+ function extractNonEmptyString(data, key) {
174
+ const value = data[key];
175
+ if (typeof value !== "string")
176
+ return;
177
+ const trimmed = value.trim();
178
+ return trimmed !== "" ? trimmed : undefined;
179
+ }
180
+ function extractNumber(data, key) {
181
+ const value = data[key];
182
+ return typeof value === "number" ? value : undefined;
183
+ }
184
+ function extractBoolean(data, key) {
185
+ const value = data[key];
186
+ if (typeof value === "boolean")
187
+ return value;
188
+ if (typeof value === "string") {
189
+ const normalized = value.trim().toLowerCase();
190
+ if (normalized === "true")
191
+ return true;
192
+ if (normalized === "false")
193
+ return false;
194
+ }
195
+ return;
102
196
  }
103
197
 
104
198
  // src/lib/walk-dir.ts
@@ -147,11 +241,23 @@ function findAgentsInDir(dir, maxDepth = 2) {
147
241
  }));
148
242
  }
149
243
  function extractAgentFrontmatter(content) {
150
- const { data, parseError } = parseFrontmatter(content);
244
+ const { data, parseError, body } = parseFrontmatter(content);
245
+ if (parseError) {
246
+ return { name: "", description: "", prompt: body.trim() };
247
+ }
151
248
  return {
152
- name: !parseError && typeof data.name === "string" ? data.name : "",
153
- description: !parseError && typeof data.description === "string" ? data.description : "",
154
- prompt: stripFrontmatter(content)
249
+ name: extractString(data, "name"),
250
+ description: extractString(data, "description"),
251
+ prompt: body.trim(),
252
+ model: extractNonEmptyString(data, "model"),
253
+ temperature: extractNumber(data, "temperature"),
254
+ top_p: extractNumber(data, "top_p"),
255
+ tools: isToolsMap(data.tools) ? data.tools : undefined,
256
+ disable: extractBoolean(data, "disable"),
257
+ mode: isAgentMode(data.mode) ? data.mode : undefined,
258
+ color: extractNonEmptyString(data, "color"),
259
+ maxSteps: extractNumber(data, "maxSteps"),
260
+ permission: normalizePermission(data.permission)
155
261
  };
156
262
  }
157
263
 
@@ -173,11 +279,24 @@ function findCommandsInDir(dir, maxDepth = 2) {
173
279
  }
174
280
  function extractCommandFrontmatter(content) {
175
281
  const { data, parseError } = parseFrontmatter(content);
176
- const argumentHintRaw = !parseError && typeof data["argument-hint"] === "string" ? data["argument-hint"] : "";
282
+ if (parseError) {
283
+ return {
284
+ name: "",
285
+ description: "",
286
+ argumentHint: "",
287
+ agent: undefined,
288
+ model: undefined,
289
+ subtask: undefined
290
+ };
291
+ }
292
+ const argumentHintRaw = extractString(data, "argument-hint");
177
293
  return {
178
- name: !parseError && typeof data.name === "string" ? data.name : "",
179
- description: !parseError && typeof data.description === "string" ? data.description : "",
180
- argumentHint: argumentHintRaw.replace(/^["']|["']$/g, "")
294
+ name: extractString(data, "name"),
295
+ description: extractString(data, "description"),
296
+ argumentHint: argumentHintRaw.replace(/^["']|["']$/g, ""),
297
+ agent: extractNonEmptyString(data, "agent"),
298
+ model: extractNonEmptyString(data, "model"),
299
+ subtask: extractBoolean(data, "subtask")
181
300
  };
182
301
  }
183
302
 
@@ -293,30 +412,48 @@ function normalizeModel(model) {
293
412
  return `google/${model}`;
294
413
  return `anthropic/${model}`;
295
414
  }
415
+ function addOptionalFields(target, data) {
416
+ if (typeof data.top_p === "number")
417
+ target.top_p = data.top_p;
418
+ if (isToolsMap(data.tools))
419
+ target.tools = data.tools;
420
+ if (typeof data.disable === "boolean")
421
+ target.disable = data.disable;
422
+ if (typeof data.color === "string")
423
+ target.color = data.color;
424
+ if (typeof data.maxSteps === "number")
425
+ target.maxSteps = data.maxSteps;
426
+ const permission = normalizePermission(data.permission);
427
+ if (permission)
428
+ target.permission = permission;
429
+ }
296
430
  function transformAgentFrontmatter(data, agentMode) {
297
431
  const name = typeof data.name === "string" ? data.name : "";
298
432
  const description = typeof data.description === "string" ? data.description : "";
299
- const newData = {
300
- description: description || `${name} agent`,
301
- mode: agentMode
302
- };
433
+ const mode = isAgentMode(data.mode) ? data.mode : agentMode;
434
+ const newData = { mode };
435
+ if (description) {
436
+ newData.description = description;
437
+ } else if (name) {
438
+ newData.description = `${name} agent`;
439
+ }
303
440
  if (typeof data.model === "string" && data.model !== "inherit") {
304
441
  newData.model = normalizeModel(data.model);
305
442
  }
306
- if (typeof data.temperature === "number") {
307
- newData.temperature = data.temperature;
308
- } else {
309
- newData.temperature = inferTemperature(name, description);
310
- }
443
+ newData.temperature = typeof data.temperature === "number" ? data.temperature : inferTemperature(name, description);
444
+ addOptionalFields(newData, data);
311
445
  return newData;
312
446
  }
313
447
  function convertContent(content, type, options = {}) {
314
448
  if (content === "")
315
449
  return "";
316
- const { data, body, hadFrontmatter } = parseFrontmatter(content);
450
+ const { data, body, hadFrontmatter, parseError } = parseFrontmatter(content);
317
451
  if (!hadFrontmatter) {
318
452
  return options.skipBodyTransform ? content : transformBody(content);
319
453
  }
454
+ if (parseError) {
455
+ return content;
456
+ }
320
457
  const shouldTransformBody = !options.skipBodyTransform;
321
458
  const transformedBody = shouldTransformBody ? transformBody(body) : body;
322
459
  if (type === "agent") {
@@ -412,4 +549,4 @@ ${skillsXml}
412
549
  </available_skills>`;
413
550
  }
414
551
 
415
- export { stripFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir, formatSkillsXml };
552
+ export { parseFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir, formatSkillsXml };
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  findSkillsInDir,
9
9
  formatSkillsXml,
10
10
  loadConfig,
11
- stripFrontmatter
12
- } from "./index-kjhs9jeg.js";
11
+ parseFrontmatter
12
+ } from "./index-95qwq9ph.js";
13
13
 
14
14
  // src/index.ts
15
15
  import fs2 from "fs";
@@ -54,7 +54,8 @@ function getBootstrapContent(config, deps) {
54
54
  if (!fs.existsSync(usingSystematicPath))
55
55
  return null;
56
56
  const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
57
- const content = stripFrontmatter(fullContent);
57
+ const { body } = parseFrontmatter(fullContent);
58
+ const content = body.trim();
58
59
  const toolMapping = getToolMappingTemplate(bundledSkillsDir);
59
60
  return `<SYSTEMATIC_WORKFLOWS>
60
61
  You have access to structured engineering workflows via the systematic plugin.
@@ -106,7 +107,7 @@ function loadSkill(skillInfo) {
106
107
  const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
107
108
  source: "bundled"
108
109
  });
109
- const body = stripFrontmatter(converted);
110
+ const { body } = parseFrontmatter(converted);
110
111
  const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
111
112
  return {
112
113
  name: skillInfo.name,
@@ -128,11 +129,42 @@ function loadAgentAsConfig(agentInfo) {
128
129
  source: "bundled",
129
130
  agentMode: "subagent"
130
131
  });
131
- const { description, prompt } = extractAgentFrontmatter(converted);
132
- return {
132
+ const {
133
+ description,
134
+ prompt,
135
+ model,
136
+ temperature,
137
+ top_p,
138
+ tools,
139
+ disable,
140
+ mode,
141
+ color,
142
+ maxSteps,
143
+ permission
144
+ } = extractAgentFrontmatter(converted);
145
+ const config = {
133
146
  description: description || `${agentInfo.name} agent`,
134
- prompt: prompt || stripFrontmatter(converted)
147
+ prompt
135
148
  };
149
+ if (model !== undefined)
150
+ config.model = model;
151
+ if (temperature !== undefined)
152
+ config.temperature = temperature;
153
+ if (top_p !== undefined)
154
+ config.top_p = top_p;
155
+ if (tools !== undefined)
156
+ config.tools = tools;
157
+ if (disable !== undefined)
158
+ config.disable = disable;
159
+ if (mode !== undefined)
160
+ config.mode = mode;
161
+ if (color !== undefined)
162
+ config.color = color;
163
+ if (maxSteps !== undefined)
164
+ config.maxSteps = maxSteps;
165
+ if (permission !== undefined)
166
+ config.permission = permission;
167
+ return config;
136
168
  } catch {
137
169
  return null;
138
170
  }
@@ -142,12 +174,20 @@ function loadCommandAsConfig(commandInfo) {
142
174
  const converted = convertFileWithCache(commandInfo.file, "command", {
143
175
  source: "bundled"
144
176
  });
145
- const { name, description } = extractCommandFrontmatter(converted);
177
+ const { name, description, agent, model, subtask } = extractCommandFrontmatter(converted);
178
+ const { body } = parseFrontmatter(converted);
146
179
  const cleanName = commandInfo.name.replace(/^\//, "");
147
- return {
148
- template: stripFrontmatter(converted),
180
+ const config = {
181
+ template: body.trim(),
149
182
  description: description || `${name || cleanName} command`
150
183
  };
184
+ if (agent !== undefined)
185
+ config.agent = agent;
186
+ if (model !== undefined)
187
+ config.model = model;
188
+ if (subtask !== undefined)
189
+ config.subtask = subtask;
190
+ return config;
151
191
  } catch {
152
192
  return null;
153
193
  }
@@ -1,7 +1,29 @@
1
+ import { type PermissionConfig } from './validation.js';
1
2
  export interface AgentFrontmatter {
3
+ /** Name of the agent */
2
4
  name: string;
5
+ /** Description of the agent's purpose */
3
6
  description: string;
7
+ /** The system prompt for the agent */
4
8
  prompt: string;
9
+ /** Model to use (provider/model format) */
10
+ model?: string;
11
+ /** Temperature for generation */
12
+ temperature?: number;
13
+ /** Top-p sampling */
14
+ top_p?: number;
15
+ /** Tool whitelist/blacklist */
16
+ tools?: Record<string, boolean>;
17
+ /** Disable this agent */
18
+ disable?: boolean;
19
+ /** Agent mode */
20
+ mode?: 'subagent' | 'primary' | 'all';
21
+ /** Hex color code */
22
+ color?: string;
23
+ /** Max agentic iterations */
24
+ maxSteps?: number;
25
+ /** Permission settings */
26
+ permission?: PermissionConfig;
5
27
  }
6
28
  export interface AgentInfo {
7
29
  name: string;
@@ -2,6 +2,12 @@ export interface CommandFrontmatter {
2
2
  name: string;
3
3
  description: string;
4
4
  argumentHint: string;
5
+ /** Agent ID to use for this command */
6
+ agent?: string;
7
+ /** Model override for this command */
8
+ model?: string;
9
+ /** Whether this command should run as a subtask */
10
+ subtask?: boolean;
5
11
  }
6
12
  export interface CommandInfo {
7
13
  name: string;
@@ -1,6 +1,6 @@
1
+ import { type AgentMode } from './validation.js';
1
2
  export type ContentType = 'skill' | 'agent' | 'command';
2
3
  export type SourceType = 'bundled' | 'external';
3
- export type AgentMode = 'primary' | 'subagent';
4
4
  export interface ConvertOptions {
5
5
  source?: SourceType;
6
6
  agentMode?: AgentMode;
@@ -15,4 +15,8 @@ export interface FrontmatterResult<T = Record<string, unknown>> {
15
15
  */
16
16
  export declare function parseFrontmatter<T = Record<string, unknown>>(content: string): FrontmatterResult<T>;
17
17
  export declare function formatFrontmatter(data: Record<string, unknown>): string;
18
+ /**
19
+ * Removes YAML frontmatter from content and returns the body.
20
+ * Convenience wrapper around parseFrontmatter.
21
+ */
18
22
  export declare function stripFrontmatter(content: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared type guards and validation utilities for agent/skill/command frontmatter.
3
+ */
4
+ export type AgentMode = 'subagent' | 'primary' | 'all';
5
+ export type PermissionSetting = 'ask' | 'allow' | 'deny';
6
+ export interface PermissionConfig {
7
+ edit?: PermissionSetting;
8
+ bash?: PermissionSetting | Record<string, PermissionSetting>;
9
+ webfetch?: PermissionSetting;
10
+ doom_loop?: PermissionSetting;
11
+ external_directory?: PermissionSetting;
12
+ }
13
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
14
+ export declare function isPermissionSetting(value: unknown): value is PermissionSetting;
15
+ export declare function isToolsMap(value: unknown): value is Record<string, boolean>;
16
+ export declare function isAgentMode(value: unknown): value is AgentMode;
17
+ export declare function normalizePermission(value: unknown): PermissionConfig | undefined;
18
+ /**
19
+ * Shared frontmatter extraction helpers.
20
+ * Centralized to ensure consistent behavior across agents, commands, and skills.
21
+ */
22
+ export declare function extractString(data: Record<string, unknown>, key: string, fallback?: string): string;
23
+ export declare function extractNonEmptyString(data: Record<string, unknown>, key: string): string | undefined;
24
+ export declare function extractNumber(data: Record<string, unknown>, key: string): number | undefined;
25
+ export declare function extractBoolean(data: Record<string, unknown>, key: string): boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Structured engineering workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",