@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 CHANGED
@@ -82,9 +82,9 @@ The plugin provides these tools to OpenCode:
82
82
 
83
83
  | Tool | Description |
84
84
  |------|-------------|
85
- | `systematic_find_skills` | List available skills |
86
- | `systematic_find_agents` | List available agents |
87
- | `systematic_find_commands` | List available commands |
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 fs3 from "node:fs";
11
+ import fs4 from "node:fs";
12
12
  import os2 from "node:os";
13
- import path2 from "node:path";
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/index.ts
169
- var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
170
- var packageRoot = path2.resolve(__dirname2, "..");
171
- var bundledSkillsDir = path2.join(packageRoot, "skills");
172
- var bundledAgentsDir = path2.join(packageRoot, "agents");
173
- var bundledCommandsDir = path2.join(packageRoot, "commands");
174
- function formatItemList(items, emptyMessage, header) {
175
- if (items.length === 0)
176
- return emptyMessage;
177
- let output = header;
178
- for (const item of items) {
179
- output += `- ${item.name} (${item.sourceType})
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 output;
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("~/") ? path2.join(os2.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
189
- if (fs3.existsSync(customPath)) {
190
- return fs3.readFileSync(customPath, "utf8");
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 = path2.join(bundledSkillsDir, "using-systematic/SKILL.md");
194
- if (!fs3.existsSync(usingSystematicPath))
325
+ const usingSystematicPath = path3.join(bundledSkillsDir, "using-systematic/SKILL.md");
326
+ if (!fs4.existsSync(usingSystematicPath))
195
327
  return null;
196
- const fullContent = fs3.readFileSync(usingSystematicPath, "utf8");
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 skill tool to load "using-systematic" again - that would be redundant.**
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
- systematic_find_skills: tool({
234
- description: "List all available skills in the bundled skill library.",
235
- args: {},
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",
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": "^1.9.0",
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 `skill` tool" [shape=box];
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 `skill` tool" [label="yes, even 1%"];
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 `skill` tool" -> "Announce: 'Using [skill] to [purpose]'";
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
- Use `systematic_find_skills` to see all available skills and their sources.
94
+ Systematic bundled skills are listed in the `systematic_skill` tool description. Use the native `skill` tool for skills outside the Systematic plugin.