@fro.bot/systematic 1.0.3 → 1.1.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/README.md +3 -3
- package/dist/index.js +193 -76
- package/package.json +2 -2
- package/skills/using-systematic/SKILL.md +5 -5
package/README.md
CHANGED
|
@@ -82,9 +82,9 @@ The plugin provides these tools to OpenCode:
|
|
|
82
82
|
|
|
83
83
|
| Tool | Description |
|
|
84
84
|
|------|-------------|
|
|
85
|
-
| `
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
| `systematic_skill` | Load Systematic bundled skills |
|
|
86
|
+
|
|
87
|
+
The bootstrap skill instructs OpenCode to use the native `skill` tool to load non-Systematic skills.
|
|
88
88
|
|
|
89
89
|
## Configuration
|
|
90
90
|
|
package/dist/index.js
CHANGED
|
@@ -8,14 +8,10 @@ import {
|
|
|
8
8
|
} from "./index-v8dhd5s2.js";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
|
-
import
|
|
11
|
+
import fs4 from "node:fs";
|
|
12
12
|
import os2 from "node:os";
|
|
13
|
-
import
|
|
13
|
+
import path3 from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { tool } from "@opencode-ai/plugin/tool";
|
|
16
|
-
|
|
17
|
-
// src/lib/config-handler.ts
|
|
18
|
-
import fs2 from "node:fs";
|
|
19
15
|
|
|
20
16
|
// src/lib/config.ts
|
|
21
17
|
import fs from "node:fs";
|
|
@@ -68,6 +64,7 @@ function loadConfig(projectDir) {
|
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
// src/lib/config-handler.ts
|
|
67
|
+
import fs2 from "node:fs";
|
|
71
68
|
function loadAgentAsConfig(agentInfo) {
|
|
72
69
|
try {
|
|
73
70
|
const content = fs2.readFileSync(agentInfo.file, "utf8");
|
|
@@ -165,54 +162,193 @@ function createConfigHandler(deps) {
|
|
|
165
162
|
};
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
// src/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
var
|
|
173
|
-
var
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
// src/lib/skill-tool.ts
|
|
166
|
+
import fs3 from "node:fs";
|
|
167
|
+
import path2 from "node:path";
|
|
168
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
169
|
+
var HOOK_KEY = "systematic_skill_tool_hooked";
|
|
170
|
+
var SYSTEMATIC_MARKER = "__systematic_skill_tool__";
|
|
171
|
+
var globalStore = globalThis;
|
|
172
|
+
function getHookState() {
|
|
173
|
+
let state = globalStore[HOOK_KEY];
|
|
174
|
+
if (state == null) {
|
|
175
|
+
state = {
|
|
176
|
+
hookedTool: null,
|
|
177
|
+
hookedDescription: null,
|
|
178
|
+
initialized: false
|
|
179
|
+
};
|
|
180
|
+
globalStore[HOOK_KEY] = state;
|
|
181
181
|
}
|
|
182
|
-
return
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
function getHookedTool() {
|
|
185
|
+
return getHookState().hookedTool;
|
|
186
|
+
}
|
|
187
|
+
function formatSkillsXml(skills) {
|
|
188
|
+
if (skills.length === 0)
|
|
189
|
+
return "";
|
|
190
|
+
const skillsXml = skills.map((skill) => {
|
|
191
|
+
const lines = [
|
|
192
|
+
" <skill>",
|
|
193
|
+
` <name>systematic:${skill.name}</name>`,
|
|
194
|
+
` <description>${skill.description}</description>`
|
|
195
|
+
];
|
|
196
|
+
lines.push(" </skill>");
|
|
197
|
+
return lines.join(`
|
|
198
|
+
`);
|
|
199
|
+
}).join(`
|
|
200
|
+
`);
|
|
201
|
+
return `<available_skills>
|
|
202
|
+
${skillsXml}
|
|
203
|
+
</available_skills>`;
|
|
204
|
+
}
|
|
205
|
+
function mergeDescriptions(baseDescription, hookedDescription, systematicSkillsXml) {
|
|
206
|
+
if (hookedDescription == null || hookedDescription.trim() === "") {
|
|
207
|
+
return `${baseDescription}
|
|
208
|
+
|
|
209
|
+
${systematicSkillsXml}`;
|
|
210
|
+
}
|
|
211
|
+
const availableSkillsMatch = hookedDescription.match(/<available_skills>([\s\S]*?)<\/available_skills>/);
|
|
212
|
+
if (availableSkillsMatch) {
|
|
213
|
+
const existingSkillsContent = availableSkillsMatch[1];
|
|
214
|
+
const systematicSkillsContent = systematicSkillsXml.replace("<available_skills>", "").replace("</available_skills>", "").trim();
|
|
215
|
+
const mergedContent = `<available_skills>
|
|
216
|
+
${systematicSkillsContent}
|
|
217
|
+
${existingSkillsContent}</available_skills>`;
|
|
218
|
+
return hookedDescription.replace(/<available_skills>[\s\S]*?<\/available_skills>/, mergedContent);
|
|
219
|
+
}
|
|
220
|
+
return `${hookedDescription}
|
|
221
|
+
|
|
222
|
+
${systematicSkillsXml}`;
|
|
223
|
+
}
|
|
224
|
+
function wrapSkillContent(skillPath, content) {
|
|
225
|
+
const skillDir = path2.dirname(skillPath);
|
|
226
|
+
const body = stripFrontmatter(content);
|
|
227
|
+
return `<skill_instruction>
|
|
228
|
+
Base directory for this skill: ${skillDir}/
|
|
229
|
+
File references (@path) in this skill are relative to this directory.
|
|
230
|
+
|
|
231
|
+
${body.trim()}
|
|
232
|
+
</skill_instruction>`;
|
|
233
|
+
}
|
|
234
|
+
function createSkillTool(options) {
|
|
235
|
+
const { bundledSkillsDir, disabledSkills } = options;
|
|
236
|
+
const getSystematicSkills = () => {
|
|
237
|
+
return findSkillsInDir(bundledSkillsDir, "bundled", 3).filter((s) => !disabledSkills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
238
|
+
};
|
|
239
|
+
const buildDescription = () => {
|
|
240
|
+
const skills = getSystematicSkills();
|
|
241
|
+
const systematicXml = formatSkillsXml(skills);
|
|
242
|
+
const baseDescription = `Load a skill to get detailed instructions for a specific task.
|
|
243
|
+
|
|
244
|
+
Skills provide specialized knowledge and step-by-step guidance.
|
|
245
|
+
Use this when a task matches an available skill's description.`;
|
|
246
|
+
const hookState = getHookState();
|
|
247
|
+
return mergeDescriptions(baseDescription, hookState.hookedDescription, systematicXml);
|
|
248
|
+
};
|
|
249
|
+
let cachedDescription = null;
|
|
250
|
+
const toolDef = tool({
|
|
251
|
+
get description() {
|
|
252
|
+
if (cachedDescription == null) {
|
|
253
|
+
cachedDescription = buildDescription();
|
|
254
|
+
}
|
|
255
|
+
return cachedDescription;
|
|
256
|
+
},
|
|
257
|
+
args: {
|
|
258
|
+
name: tool.schema.string().describe("The skill identifier from available_skills (e.g., 'systematic:brainstorming')")
|
|
259
|
+
},
|
|
260
|
+
async execute(args) {
|
|
261
|
+
const requestedName = args.name;
|
|
262
|
+
const normalizedName = requestedName.startsWith("systematic:") ? requestedName.slice("systematic:".length) : requestedName;
|
|
263
|
+
const skills = getSystematicSkills();
|
|
264
|
+
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
265
|
+
if (matchedSkill) {
|
|
266
|
+
try {
|
|
267
|
+
const content = fs3.readFileSync(matchedSkill.skillFile, "utf8");
|
|
268
|
+
const wrapped = wrapSkillContent(matchedSkill.skillFile, content);
|
|
269
|
+
return `## Skill: systematic:${matchedSkill.name}
|
|
270
|
+
|
|
271
|
+
**Base directory**: ${matchedSkill.path}
|
|
272
|
+
|
|
273
|
+
${wrapped}`;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
276
|
+
throw new Error(`Failed to load skill "${requestedName}": ${errorMessage}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const hookedTool = getHookedTool();
|
|
280
|
+
if (hookedTool != null && typeof hookedTool.execute === "function") {
|
|
281
|
+
try {
|
|
282
|
+
return await hookedTool.execute(args);
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
const availableSystematic = skills.map((s) => `systematic:${s.name}`);
|
|
286
|
+
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
Object.defineProperty(toolDef, SYSTEMATIC_MARKER, {
|
|
290
|
+
value: true,
|
|
291
|
+
enumerable: false,
|
|
292
|
+
writable: false
|
|
293
|
+
});
|
|
294
|
+
return toolDef;
|
|
183
295
|
}
|
|
296
|
+
|
|
297
|
+
// src/index.ts
|
|
298
|
+
var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
|
|
299
|
+
var packageRoot = path3.resolve(__dirname2, "..");
|
|
300
|
+
var bundledSkillsDir = path3.join(packageRoot, "skills");
|
|
301
|
+
var bundledAgentsDir = path3.join(packageRoot, "agents");
|
|
302
|
+
var bundledCommandsDir = path3.join(packageRoot, "commands");
|
|
303
|
+
var packageJsonPath = path3.join(packageRoot, "package.json");
|
|
304
|
+
var hasLoggedInit = false;
|
|
305
|
+
var getPackageVersion = () => {
|
|
306
|
+
try {
|
|
307
|
+
if (!fs4.existsSync(packageJsonPath))
|
|
308
|
+
return "unknown";
|
|
309
|
+
const content = fs4.readFileSync(packageJsonPath, "utf8");
|
|
310
|
+
const parsed = JSON.parse(content);
|
|
311
|
+
return parsed.version ?? "unknown";
|
|
312
|
+
} catch {
|
|
313
|
+
return "unknown";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
184
316
|
var getBootstrapContent = (config) => {
|
|
185
317
|
if (!config.bootstrap.enabled)
|
|
186
318
|
return null;
|
|
187
319
|
if (config.bootstrap.file) {
|
|
188
|
-
const customPath = config.bootstrap.file.startsWith("~/") ?
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
320
|
+
const customPath = config.bootstrap.file.startsWith("~/") ? path3.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
321
|
+
if (fs4.existsSync(customPath)) {
|
|
322
|
+
return fs4.readFileSync(customPath, "utf8");
|
|
191
323
|
}
|
|
192
324
|
}
|
|
193
|
-
const usingSystematicPath =
|
|
194
|
-
if (!
|
|
325
|
+
const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
326
|
+
if (!fs4.existsSync(usingSystematicPath))
|
|
195
327
|
return null;
|
|
196
|
-
const fullContent =
|
|
328
|
+
const fullContent = fs4.readFileSync(usingSystematicPath, "utf8");
|
|
197
329
|
const content = stripFrontmatter(fullContent);
|
|
198
330
|
const toolMapping = `**Tool Mapping for OpenCode:**
|
|
199
331
|
When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
200
332
|
- \`TodoWrite\` → \`update_plan\`
|
|
201
333
|
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
|
|
202
334
|
- \`Skill\` tool → OpenCode's native \`skill\` tool
|
|
335
|
+
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
|
|
203
336
|
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
|
|
204
337
|
|
|
205
338
|
**Skills naming:**
|
|
206
339
|
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
|
|
207
340
|
- Skills can also be invoked without prefix if unambiguous
|
|
208
341
|
|
|
342
|
+
**Skills usage:**
|
|
343
|
+
- Use \`systematic_skill\` to load Systematic bundled skills
|
|
344
|
+
- Use the native \`skill\` tool for non-Systematic skills
|
|
345
|
+
|
|
209
346
|
**Skills location:**
|
|
210
|
-
Bundled skills are in \`${bundledSkillsDir}
|
|
211
|
-
Use \`systematic_find_skills\` to list all available skills.`;
|
|
347
|
+
Bundled skills are in \`${bundledSkillsDir}/\``;
|
|
212
348
|
return `<SYSTEMATIC_WORKFLOWS>
|
|
213
349
|
You have access to structured engineering workflows via the systematic plugin.
|
|
214
350
|
|
|
215
|
-
**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the
|
|
351
|
+
**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the systematic_skill tool to load "using-systematic" again - that would be redundant.**
|
|
216
352
|
|
|
217
353
|
${content}
|
|
218
354
|
|
|
@@ -230,56 +366,37 @@ var SystematicPlugin = async ({ client, directory }) => {
|
|
|
230
366
|
return {
|
|
231
367
|
config: configHandler,
|
|
232
368
|
tool: {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
execute: async () => {
|
|
237
|
-
const bundledSkills = findSkillsInDir(bundledSkillsDir, "bundled", 3);
|
|
238
|
-
const skills = bundledSkills.filter((s) => !config.disabled_skills.includes(s.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
239
|
-
if (skills.length === 0) {
|
|
240
|
-
return "No skills available. Skills are bundled with the systematic plugin.";
|
|
241
|
-
}
|
|
242
|
-
let output = `Available skills:
|
|
243
|
-
|
|
244
|
-
`;
|
|
245
|
-
for (const skill of skills) {
|
|
246
|
-
output += `systematic:${skill.name}
|
|
247
|
-
`;
|
|
248
|
-
if (skill.description) {
|
|
249
|
-
output += ` ${skill.description}
|
|
250
|
-
`;
|
|
251
|
-
}
|
|
252
|
-
output += ` Directory: ${skill.path}
|
|
253
|
-
|
|
254
|
-
`;
|
|
255
|
-
}
|
|
256
|
-
return output.trim();
|
|
257
|
-
}
|
|
258
|
-
}),
|
|
259
|
-
systematic_find_agents: tool({
|
|
260
|
-
description: "List all available review agents.",
|
|
261
|
-
args: {},
|
|
262
|
-
execute: async () => {
|
|
263
|
-
const bundledAgents = findAgentsInDir(bundledAgentsDir, "bundled");
|
|
264
|
-
const agents = bundledAgents.filter((a) => !config.disabled_agents.includes(a.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
265
|
-
return formatItemList(agents, "No agents available.", `Available agents:
|
|
266
|
-
|
|
267
|
-
`);
|
|
268
|
-
}
|
|
269
|
-
}),
|
|
270
|
-
systematic_find_commands: tool({
|
|
271
|
-
description: "List all available commands.",
|
|
272
|
-
args: {},
|
|
273
|
-
execute: async () => {
|
|
274
|
-
const bundledCommands = findCommandsInDir(bundledCommandsDir, "bundled");
|
|
275
|
-
const commands = bundledCommands.filter((c) => !config.disabled_commands.includes(c.name.replace(/^\//, ""))).sort((a, b) => a.name.localeCompare(b.name));
|
|
276
|
-
return formatItemList(commands, "No commands available.", `Available commands:
|
|
277
|
-
|
|
278
|
-
`);
|
|
279
|
-
}
|
|
369
|
+
systematic_skill: createSkillTool({
|
|
370
|
+
bundledSkillsDir,
|
|
371
|
+
disabledSkills: config.disabled_skills
|
|
280
372
|
})
|
|
281
373
|
},
|
|
282
374
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
375
|
+
if (!hasLoggedInit) {
|
|
376
|
+
hasLoggedInit = true;
|
|
377
|
+
const packageVersion = getPackageVersion();
|
|
378
|
+
try {
|
|
379
|
+
await client.app.log({
|
|
380
|
+
body: {
|
|
381
|
+
service: "systematic",
|
|
382
|
+
level: "info",
|
|
383
|
+
message: "Systematic plugin initialized",
|
|
384
|
+
extra: {
|
|
385
|
+
version: packageVersion,
|
|
386
|
+
bootstrapEnabled: config.bootstrap.enabled,
|
|
387
|
+
disabledSkillsCount: config.disabled_skills.length,
|
|
388
|
+
disabledAgentsCount: config.disabled_agents.length,
|
|
389
|
+
disabledCommandsCount: config.disabled_commands.length
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
} catch {}
|
|
394
|
+
}
|
|
395
|
+
const existingSystem = output.system.join(`
|
|
396
|
+
`).toLowerCase();
|
|
397
|
+
if (existingSystem.includes("title generator") || existingSystem.includes("generate a title")) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
283
400
|
const content = getBootstrapContent(config);
|
|
284
401
|
if (content) {
|
|
285
402
|
if (!output.system) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@opencode-ai/plugin": "^1.1.30"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "^
|
|
53
|
+
"@biomejs/biome": "^2.0.0",
|
|
54
54
|
"@opencode-ai/plugin": "^1.1.30",
|
|
55
55
|
"@types/bun": "latest",
|
|
56
56
|
"@types/node": "^22.0.0",
|
|
@@ -13,7 +13,7 @@ This is not negotiable. This is not optional. You cannot rationalize your way ou
|
|
|
13
13
|
|
|
14
14
|
## How to Access Skills
|
|
15
15
|
|
|
16
|
-
Use the `skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly.
|
|
16
|
+
Use the `systematic_skill` tool for Systematic bundled skills. Use the native `skill` tool for non-Systematic skills. When you invoke a skill, its content is loaded and presented to you—follow it directly.
|
|
17
17
|
|
|
18
18
|
# Using Skills
|
|
19
19
|
|
|
@@ -25,7 +25,7 @@ Use the `skill` tool. When you invoke a skill, its content is loaded and present
|
|
|
25
25
|
digraph skill_flow {
|
|
26
26
|
"User message received" [shape=doublecircle];
|
|
27
27
|
"Might any skill apply?" [shape=diamond];
|
|
28
|
-
"Invoke `
|
|
28
|
+
"Invoke `systematic_skill` tool" [shape=box];
|
|
29
29
|
"Announce: 'Using [skill] to [purpose]'" [shape=box];
|
|
30
30
|
"Has checklist?" [shape=diamond];
|
|
31
31
|
"Create todo per item" [shape=box];
|
|
@@ -33,9 +33,9 @@ digraph skill_flow {
|
|
|
33
33
|
"Respond (including clarifications)" [shape=doublecircle];
|
|
34
34
|
|
|
35
35
|
"User message received" -> "Might any skill apply?";
|
|
36
|
-
"Might any skill apply?" -> "Invoke `
|
|
36
|
+
"Might any skill apply?" -> "Invoke `systematic_skill` tool" [label="yes, even 1%"];
|
|
37
37
|
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
|
|
38
|
-
"Invoke `
|
|
38
|
+
"Invoke `systematic_skill` tool" -> "Announce: 'Using [skill] to [purpose]'";
|
|
39
39
|
"Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
|
|
40
40
|
"Has checklist?" -> "Create todo per item" [label="yes"];
|
|
41
41
|
"Has checklist?" -> "Follow skill exactly" [label="no"];
|
|
@@ -91,4 +91,4 @@ Skills are resolved in priority order:
|
|
|
91
91
|
2. **User skills**: `~/.config/opencode/skills/`
|
|
92
92
|
3. **Bundled skills**: Provided by systematic plugin
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Systematic bundled skills are listed in the `systematic_skill` tool description. Use the native `skill` tool for skills outside the Systematic plugin.
|