@fro.bot/systematic 1.5.0 → 1.7.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-yxbcy3s7.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") {
@@ -365,9 +502,29 @@ function extractFrontmatter(filePath) {
365
502
  if (parseError) {
366
503
  return { name: "", description: "" };
367
504
  }
505
+ const metadataRaw = data.metadata;
506
+ let metadata;
507
+ if (isRecord(metadataRaw)) {
508
+ const entries = Object.entries(metadataRaw);
509
+ if (entries.every(([, v]) => typeof v === "string")) {
510
+ metadata = Object.fromEntries(entries);
511
+ }
512
+ }
513
+ const argumentHintRaw = extractNonEmptyString(data, "argument-hint");
514
+ const argumentHint = argumentHintRaw?.replace(/^["']|["']$/g, "") || undefined;
368
515
  return {
369
- name: typeof data.name === "string" ? data.name : "",
370
- description: typeof data.description === "string" ? data.description : ""
516
+ name: extractString(data, "name"),
517
+ description: extractString(data, "description"),
518
+ license: extractNonEmptyString(data, "license"),
519
+ compatibility: extractNonEmptyString(data, "compatibility"),
520
+ metadata,
521
+ disableModelInvocation: extractBoolean(data, "disable-model-invocation"),
522
+ userInvocable: extractBoolean(data, "user-invocable"),
523
+ subtask: data.context === "fork" ? true : undefined,
524
+ agent: extractNonEmptyString(data, "agent"),
525
+ model: extractNonEmptyString(data, "model"),
526
+ argumentHint: argumentHint !== "" ? argumentHint : undefined,
527
+ allowedTools: extractNonEmptyString(data, "allowed-tools")
371
528
  };
372
529
  } catch {
373
530
  return { name: "", description: "" };
@@ -382,34 +539,26 @@ function findSkillsInDir(dir, maxDepth = 3) {
382
539
  for (const entry of entries) {
383
540
  const skillFile = path3.join(entry.path, "SKILL.md");
384
541
  if (fs4.existsSync(skillFile)) {
385
- const { name, description } = extractFrontmatter(skillFile);
542
+ const frontmatter = extractFrontmatter(skillFile);
386
543
  skills.push({
387
544
  path: entry.path,
388
545
  skillFile,
389
- name: name || entry.name,
390
- description: description || ""
546
+ name: frontmatter.name || entry.name,
547
+ description: frontmatter.description || "",
548
+ license: frontmatter.license,
549
+ compatibility: frontmatter.compatibility,
550
+ metadata: frontmatter.metadata,
551
+ disableModelInvocation: frontmatter.disableModelInvocation,
552
+ userInvocable: frontmatter.userInvocable,
553
+ subtask: frontmatter.subtask,
554
+ agent: frontmatter.agent,
555
+ model: frontmatter.model,
556
+ argumentHint: frontmatter.argumentHint,
557
+ allowedTools: frontmatter.allowedTools
391
558
  });
392
559
  }
393
560
  }
394
561
  return skills;
395
562
  }
396
- function formatSkillsXml(skills) {
397
- if (skills.length === 0)
398
- return "";
399
- const skillsXml = skills.map((skill) => {
400
- const lines = [
401
- " <skill>",
402
- ` <name>systematic:${skill.name}</name>`,
403
- ` <description>${skill.description}</description>`
404
- ];
405
- lines.push(" </skill>");
406
- return lines.join(`
407
- `);
408
- }).join(`
409
- `);
410
- return `<available_skills>
411
- ${skillsXml}
412
- </available_skills>`;
413
- }
414
563
 
415
- export { stripFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir, formatSkillsXml };
564
+ export { parseFrontmatter, loadConfig, getConfigPaths, findAgentsInDir, extractAgentFrontmatter, findCommandsInDir, extractCommandFrontmatter, convertContent, convertFileWithCache, findSkillsInDir };
package/dist/index.js CHANGED
@@ -6,10 +6,9 @@ import {
6
6
  findAgentsInDir,
7
7
  findCommandsInDir,
8
8
  findSkillsInDir,
9
- formatSkillsXml,
10
9
  loadConfig,
11
- stripFrontmatter
12
- } from "./index-kjhs9jeg.js";
10
+ parseFrontmatter
11
+ } from "./index-yxbcy3s7.js";
13
12
 
14
13
  // src/index.ts
15
14
  import fs2 from "fs";
@@ -54,7 +53,8 @@ function getBootstrapContent(config, deps) {
54
53
  if (!fs.existsSync(usingSystematicPath))
55
54
  return null;
56
55
  const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
57
- const content = stripFrontmatter(fullContent);
56
+ const { body } = parseFrontmatter(fullContent);
57
+ const content = body.trim();
58
58
  const toolMapping = getToolMappingTemplate(bundledSkillsDir);
59
59
  return `<SYSTEMATIC_WORKFLOWS>
60
60
  You have access to structured engineering workflows via the systematic plugin.
@@ -106,7 +106,7 @@ function loadSkill(skillInfo) {
106
106
  const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
107
107
  source: "bundled"
108
108
  });
109
- const body = stripFrontmatter(converted);
109
+ const { body } = parseFrontmatter(converted);
110
110
  const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
111
111
  return {
112
112
  name: skillInfo.name,
@@ -114,7 +114,13 @@ function loadSkill(skillInfo) {
114
114
  description: formatSkillDescription(skillInfo.description, skillInfo.name),
115
115
  path: skillInfo.path,
116
116
  skillFile: skillInfo.skillFile,
117
- wrappedTemplate
117
+ wrappedTemplate,
118
+ disableModelInvocation: skillInfo.disableModelInvocation,
119
+ userInvocable: skillInfo.userInvocable,
120
+ subtask: skillInfo.subtask,
121
+ agent: skillInfo.agent,
122
+ model: skillInfo.model,
123
+ argumentHint: skillInfo.argumentHint
118
124
  };
119
125
  } catch {
120
126
  return null;
@@ -128,11 +134,42 @@ function loadAgentAsConfig(agentInfo) {
128
134
  source: "bundled",
129
135
  agentMode: "subagent"
130
136
  });
131
- const { description, prompt } = extractAgentFrontmatter(converted);
132
- return {
137
+ const {
138
+ description,
139
+ prompt,
140
+ model,
141
+ temperature,
142
+ top_p,
143
+ tools,
144
+ disable,
145
+ mode,
146
+ color,
147
+ maxSteps,
148
+ permission
149
+ } = extractAgentFrontmatter(converted);
150
+ const config = {
133
151
  description: description || `${agentInfo.name} agent`,
134
- prompt: prompt || stripFrontmatter(converted)
152
+ prompt
135
153
  };
154
+ if (model !== undefined)
155
+ config.model = model;
156
+ if (temperature !== undefined)
157
+ config.temperature = temperature;
158
+ if (top_p !== undefined)
159
+ config.top_p = top_p;
160
+ if (tools !== undefined)
161
+ config.tools = tools;
162
+ if (disable !== undefined)
163
+ config.disable = disable;
164
+ if (mode !== undefined)
165
+ config.mode = mode;
166
+ if (color !== undefined)
167
+ config.color = color;
168
+ if (maxSteps !== undefined)
169
+ config.maxSteps = maxSteps;
170
+ if (permission !== undefined)
171
+ config.permission = permission;
172
+ return config;
136
173
  } catch {
137
174
  return null;
138
175
  }
@@ -142,21 +179,36 @@ function loadCommandAsConfig(commandInfo) {
142
179
  const converted = convertFileWithCache(commandInfo.file, "command", {
143
180
  source: "bundled"
144
181
  });
145
- const { name, description } = extractCommandFrontmatter(converted);
182
+ const { name, description, agent, model, subtask } = extractCommandFrontmatter(converted);
183
+ const { body } = parseFrontmatter(converted);
146
184
  const cleanName = commandInfo.name.replace(/^\//, "");
147
- return {
148
- template: stripFrontmatter(converted),
185
+ const config = {
186
+ template: body.trim(),
149
187
  description: description || `${name || cleanName} command`
150
188
  };
189
+ if (agent !== undefined)
190
+ config.agent = agent;
191
+ if (model !== undefined)
192
+ config.model = model;
193
+ if (subtask !== undefined)
194
+ config.subtask = subtask;
195
+ return config;
151
196
  } catch {
152
197
  return null;
153
198
  }
154
199
  }
155
200
  function loadSkillAsCommand(loaded) {
156
- return {
201
+ const config = {
157
202
  template: loaded.wrappedTemplate,
158
203
  description: loaded.description
159
204
  };
205
+ if (loaded.agent !== undefined)
206
+ config.agent = loaded.agent;
207
+ if (loaded.model !== undefined)
208
+ config.model = loaded.model;
209
+ if (loaded.subtask !== undefined)
210
+ config.subtask = loaded.subtask;
211
+ return config;
160
212
  }
161
213
  function collectAgents(dir, disabledAgents) {
162
214
  const agents = {};
@@ -193,6 +245,8 @@ function collectSkillsAsCommands(dir, disabledSkills) {
193
245
  continue;
194
246
  const loaded = loadSkill(skillInfo);
195
247
  if (loaded) {
248
+ if (loaded.userInvocable === false)
249
+ continue;
196
250
  commands[loaded.prefixedName] = loadSkillAsCommand(loaded);
197
251
  }
198
252
  }
@@ -222,13 +276,27 @@ function createConfigHandler(deps) {
222
276
  // src/lib/skill-tool.ts
223
277
  import path3 from "path";
224
278
  import { tool } from "@opencode-ai/plugin/tool";
279
+ function formatSkillsXml(skills) {
280
+ if (skills.length === 0)
281
+ return "";
282
+ const skillLines = skills.flatMap((skill) => [
283
+ " <skill>",
284
+ ` <name>systematic:${skill.name}</name>`,
285
+ ` <description>${skill.description}</description>`,
286
+ " </skill>"
287
+ ]);
288
+ return ["<available_skills>", ...skillLines, "</available_skills>"].join(" ");
289
+ }
225
290
  function createSkillTool(options) {
226
291
  const { bundledSkillsDir, disabledSkills } = options;
227
292
  const getSystematicSkills = () => {
228
- return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).map((skillInfo) => loadSkill(skillInfo)).filter((s) => s !== null).sort((a, b) => a.name.localeCompare(b.name));
293
+ return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).map((skillInfo) => loadSkill(skillInfo)).filter((s) => s !== null).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name));
229
294
  };
230
295
  const buildDescription = () => {
231
296
  const skills = getSystematicSkills();
297
+ if (skills.length === 0) {
298
+ return "Load a skill to get detailed instructions for a specific task. No skills are currently available.";
299
+ }
232
300
  const skillInfos = skills.map((s) => ({
233
301
  name: s.name,
234
302
  description: s.description,
@@ -236,15 +304,22 @@ function createSkillTool(options) {
236
304
  skillFile: s.skillFile
237
305
  }));
238
306
  const systematicXml = formatSkillsXml(skillInfos);
239
- const baseDescription = `Load a skill to get detailed instructions for a specific task.
240
-
241
- Skills provide specialized knowledge and step-by-step guidance.
242
- Use this when a task matches an available skill's description.`;
243
- return `${baseDescription}
244
-
245
- ${systematicXml}`;
307
+ return [
308
+ "Load a skill to get detailed instructions for a specific task.",
309
+ "Skills provide specialized knowledge and step-by-step guidance.",
310
+ "Use this when a task matches an available skill's description.",
311
+ "Only the skills listed here are available:",
312
+ systematicXml
313
+ ].join(" ");
314
+ };
315
+ const buildParameterHint = () => {
316
+ const skills = getSystematicSkills();
317
+ const examples = skills.slice(0, 3).map((s) => `'systematic:${s.name}'`).join(", ");
318
+ const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : "";
319
+ return `The skill identifier from available_skills${hint}`;
246
320
  };
247
321
  let cachedDescription = null;
322
+ let cachedParameterHint = null;
248
323
  return tool({
249
324
  get description() {
250
325
  if (cachedDescription == null) {
@@ -253,24 +328,45 @@ ${systematicXml}`;
253
328
  return cachedDescription;
254
329
  },
255
330
  args: {
256
- name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'systematic:brainstorming')")
331
+ name: tool.schema.string().describe((() => {
332
+ if (cachedParameterHint == null) {
333
+ cachedParameterHint = buildParameterHint();
334
+ }
335
+ return cachedParameterHint;
336
+ })())
257
337
  },
258
- async execute(args) {
338
+ async execute(args, context) {
259
339
  const requestedName = args.name;
260
340
  const normalizedName = requestedName.startsWith("systematic:") ? requestedName.slice("systematic:".length) : requestedName;
261
341
  const skills = getSystematicSkills();
262
342
  const matchedSkill = skills.find((s) => s.name === normalizedName);
263
- if (matchedSkill) {
264
- const body = extractSkillBody(matchedSkill.wrappedTemplate);
265
- const dir = path3.dirname(matchedSkill.skillFile);
266
- return `## Skill: ${matchedSkill.prefixedName}
267
-
268
- **Base directory**: ${dir}
269
-
270
- ${body}`;
343
+ if (!matchedSkill) {
344
+ const availableSystematic = skills.map((s) => s.prefixedName);
345
+ throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
271
346
  }
272
- const availableSystematic = skills.map((s) => s.prefixedName);
273
- throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
347
+ const body = extractSkillBody(matchedSkill.wrappedTemplate);
348
+ const dir = path3.dirname(matchedSkill.skillFile);
349
+ await context.ask({
350
+ permission: "skill",
351
+ patterns: [matchedSkill.prefixedName],
352
+ always: [matchedSkill.prefixedName],
353
+ metadata: {}
354
+ });
355
+ context.metadata({
356
+ title: `Loaded skill: ${matchedSkill.prefixedName}`,
357
+ metadata: {
358
+ name: matchedSkill.prefixedName,
359
+ dir
360
+ }
361
+ });
362
+ return [
363
+ `## Skill: ${matchedSkill.prefixedName}`,
364
+ "",
365
+ `**Base directory**: ${dir}`,
366
+ "",
367
+ body.trim()
368
+ ].join(`
369
+ `);
274
370
  }
275
371
  });
276
372
  }
@@ -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;
@@ -6,6 +6,12 @@ export interface LoadedSkill {
6
6
  path: string;
7
7
  skillFile: string;
8
8
  wrappedTemplate: string;
9
+ disableModelInvocation?: boolean;
10
+ userInvocable?: boolean;
11
+ subtask?: boolean;
12
+ agent?: string;
13
+ model?: string;
14
+ argumentHint?: string;
9
15
  }
10
16
  export declare function formatSkillCommandName(name: string): string;
11
17
  export declare function formatSkillDescription(description: string, fallbackName: string): string;
@@ -1,6 +1,12 @@
1
1
  import type { ToolDefinition } from '@opencode-ai/plugin';
2
+ import { type SkillInfo } from './skills.js';
2
3
  export interface SkillToolOptions {
3
4
  bundledSkillsDir: string;
4
5
  disabledSkills: string[];
5
6
  }
7
+ /**
8
+ * Formats skills as XML for tool description.
9
+ * Uses indented format matching OpenCode's native skill tool.
10
+ */
11
+ export declare function formatSkillsXml(skills: SkillInfo[]): string;
6
12
  export declare function createSkillTool(options: SkillToolOptions): ToolDefinition;
@@ -1,13 +1,32 @@
1
1
  export interface SkillFrontmatter {
2
2
  name: string;
3
3
  description: string;
4
+ license?: string;
5
+ compatibility?: string;
6
+ metadata?: Record<string, string>;
7
+ disableModelInvocation?: boolean;
8
+ userInvocable?: boolean;
9
+ subtask?: boolean;
10
+ agent?: string;
11
+ model?: string;
12
+ argumentHint?: string;
13
+ allowedTools?: string;
4
14
  }
5
15
  export interface SkillInfo {
6
16
  path: string;
7
17
  skillFile: string;
8
18
  name: string;
9
19
  description: string;
20
+ license?: string;
21
+ compatibility?: string;
22
+ metadata?: Record<string, string>;
23
+ disableModelInvocation?: boolean;
24
+ userInvocable?: boolean;
25
+ subtask?: boolean;
26
+ agent?: string;
27
+ model?: string;
28
+ argumentHint?: string;
29
+ allowedTools?: string;
10
30
  }
11
31
  export declare function extractFrontmatter(filePath: string): SkillFrontmatter;
12
32
  export declare function findSkillsInDir(dir: string, maxDepth?: number): SkillInfo[];
13
- export declare function formatSkillsXml(skills: SkillInfo[]): 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.7.0",
4
4
  "description": "Structured engineering workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",