@fro.bot/systematic 2.13.3 → 2.14.1
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 +1 -1
- package/dist/cli.js +1 -1
- package/dist/{index-3xnyeknn.js → index-7100fhg3.js} +15547 -15543
- package/dist/index.js +193 -150
- package/dist/lib/bootstrap.d.ts +8 -3
- package/dist/lib/skill-catalog.d.ts +33 -0
- package/package.json +3 -3
- package/skills/using-systematic/SKILL.md +2 -2
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
loadConfig,
|
|
14
14
|
loadConfigWithSources,
|
|
15
15
|
parseFrontmatter
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-7100fhg3.js";
|
|
17
17
|
|
|
18
18
|
// src/index.ts
|
|
19
19
|
import fs5 from "fs";
|
|
@@ -23,7 +23,115 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
23
23
|
// src/lib/bootstrap.ts
|
|
24
24
|
import fs from "fs";
|
|
25
25
|
import os from "os";
|
|
26
|
+
import path2 from "path";
|
|
27
|
+
|
|
28
|
+
// src/lib/skill-catalog.ts
|
|
29
|
+
import { pathToFileURL } from "url";
|
|
30
|
+
|
|
31
|
+
// src/lib/skill-loader.ts
|
|
26
32
|
import path from "path";
|
|
33
|
+
var SKILL_PREFIX = "systematic:";
|
|
34
|
+
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
35
|
+
function formatSkillCommandName(name) {
|
|
36
|
+
if (name.includes(":")) {
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
return `${SKILL_PREFIX}${name}`;
|
|
40
|
+
}
|
|
41
|
+
function formatSkillDescription(description, fallbackName) {
|
|
42
|
+
const desc = description || `${fallbackName} skill`;
|
|
43
|
+
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
44
|
+
return desc;
|
|
45
|
+
}
|
|
46
|
+
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
47
|
+
}
|
|
48
|
+
function wrapSkillTemplate(skillPath, body) {
|
|
49
|
+
const skillDir = path.dirname(skillPath);
|
|
50
|
+
return `<skill-instruction>
|
|
51
|
+
Base directory for this skill: ${skillDir}/
|
|
52
|
+
File references (@path) in this skill are relative to this directory.
|
|
53
|
+
|
|
54
|
+
${body.trim()}
|
|
55
|
+
</skill-instruction>
|
|
56
|
+
|
|
57
|
+
<user-request>
|
|
58
|
+
$ARGUMENTS
|
|
59
|
+
</user-request>`;
|
|
60
|
+
}
|
|
61
|
+
function extractSkillBody(wrappedTemplate) {
|
|
62
|
+
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
63
|
+
return match ? match[1].trim() : wrappedTemplate;
|
|
64
|
+
}
|
|
65
|
+
function loadSkill(skillInfo) {
|
|
66
|
+
try {
|
|
67
|
+
const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
|
|
68
|
+
source: "bundled"
|
|
69
|
+
});
|
|
70
|
+
const { body } = parseFrontmatter(converted);
|
|
71
|
+
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
72
|
+
return {
|
|
73
|
+
name: skillInfo.name,
|
|
74
|
+
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
75
|
+
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
76
|
+
path: skillInfo.path,
|
|
77
|
+
skillFile: skillInfo.skillFile,
|
|
78
|
+
wrappedTemplate,
|
|
79
|
+
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
80
|
+
userInvocable: skillInfo.userInvocable,
|
|
81
|
+
subtask: skillInfo.subtask,
|
|
82
|
+
agent: skillInfo.agent,
|
|
83
|
+
model: skillInfo.model,
|
|
84
|
+
argumentHint: skillInfo.argumentHint
|
|
85
|
+
};
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/lib/skill-catalog.ts
|
|
92
|
+
function escapeXml(text) {
|
|
93
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
94
|
+
}
|
|
95
|
+
function buildCatalogEntries(options) {
|
|
96
|
+
const { bundledSkillsDir, disabledSkills } = options;
|
|
97
|
+
return findSkillsInDir(bundledSkillsDir).filter((s) => !disabledSkills.includes(s.name)).filter((s) => s.disableModelInvocation !== true).sort((a, b) => a.name.localeCompare(b.name)).map((s) => ({
|
|
98
|
+
name: s.name,
|
|
99
|
+
prefixedName: formatSkillCommandName(s.name),
|
|
100
|
+
description: s.description,
|
|
101
|
+
path: s.path,
|
|
102
|
+
skillFile: s.skillFile
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
function renderCatalogVerbose(options) {
|
|
106
|
+
const entries = buildCatalogEntries(options);
|
|
107
|
+
if (entries.length === 0)
|
|
108
|
+
return "";
|
|
109
|
+
const skillLines = entries.flatMap((entry) => [
|
|
110
|
+
" <skill>",
|
|
111
|
+
` <name>${escapeXml(entry.prefixedName)}</name>`,
|
|
112
|
+
` <description>${escapeXml(entry.description)}</description>`,
|
|
113
|
+
` <location>${pathToFileURL(entry.path).href}</location>`,
|
|
114
|
+
" </skill>"
|
|
115
|
+
]);
|
|
116
|
+
return ["<available_skills>", ...skillLines, "</available_skills>"].join(`
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
function renderCatalogCompact(options) {
|
|
120
|
+
const entries = buildCatalogEntries(options);
|
|
121
|
+
const heading = "## Available Systematic Skills";
|
|
122
|
+
if (entries.length === 0) {
|
|
123
|
+
return `${heading}
|
|
124
|
+
|
|
125
|
+
No Systematic skills are currently available.`;
|
|
126
|
+
}
|
|
127
|
+
const bullets = entries.map((entry) => `- ${entry.prefixedName}: ${entry.description}`).join(`
|
|
128
|
+
`);
|
|
129
|
+
return `${heading}
|
|
130
|
+
|
|
131
|
+
${bullets}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/lib/bootstrap.ts
|
|
27
135
|
var INTERNAL_AGENT_SIGNATURES = [
|
|
28
136
|
"You are a title generator",
|
|
29
137
|
"You are a helpful AI assistant tasked with summarizing conversations",
|
|
@@ -31,48 +139,64 @@ var INTERNAL_AGENT_SIGNATURES = [
|
|
|
31
139
|
];
|
|
32
140
|
var BOOTSTRAP_MARKER_OPEN = "<SYSTEMATIC_WORKFLOWS>";
|
|
33
141
|
var BOOTSTRAP_MARKER_CLOSE = "</SYSTEMATIC_WORKFLOWS>";
|
|
34
|
-
var findBootstrapMarkerBlock = (entry) => {
|
|
35
|
-
const start = entry.indexOf(BOOTSTRAP_MARKER_OPEN);
|
|
142
|
+
var findBootstrapMarkerBlock = (entry, fromIndex = 0) => {
|
|
143
|
+
const start = entry.indexOf(BOOTSTRAP_MARKER_OPEN, fromIndex);
|
|
36
144
|
if (start === -1)
|
|
37
145
|
return null;
|
|
38
146
|
const closeStart = entry.indexOf(BOOTSTRAP_MARKER_CLOSE, start + BOOTSTRAP_MARKER_OPEN.length);
|
|
39
147
|
if (closeStart === -1)
|
|
40
148
|
return null;
|
|
41
|
-
return { start, end: closeStart + BOOTSTRAP_MARKER_CLOSE.length };
|
|
149
|
+
return { start, closeStart, end: closeStart + BOOTSTRAP_MARKER_CLOSE.length };
|
|
150
|
+
};
|
|
151
|
+
var removeCompleteBootstrapBlocks = (entry) => {
|
|
152
|
+
const segments = [];
|
|
153
|
+
let cursor = 0;
|
|
154
|
+
let block = findBootstrapMarkerBlock(entry, cursor);
|
|
155
|
+
let hadNestedBlock = false;
|
|
156
|
+
while (block !== null) {
|
|
157
|
+
const nestedStart = entry.indexOf(BOOTSTRAP_MARKER_OPEN, block.start + BOOTSTRAP_MARKER_OPEN.length);
|
|
158
|
+
if (nestedStart !== -1 && nestedStart < block.closeStart) {
|
|
159
|
+
hadNestedBlock = true;
|
|
160
|
+
segments.push(entry.slice(cursor, nestedStart));
|
|
161
|
+
cursor = nestedStart;
|
|
162
|
+
block = findBootstrapMarkerBlock(entry, cursor);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
segments.push(entry.slice(cursor, block.start));
|
|
166
|
+
cursor = block.end;
|
|
167
|
+
block = findBootstrapMarkerBlock(entry, cursor);
|
|
168
|
+
}
|
|
169
|
+
if (cursor === 0)
|
|
170
|
+
return entry;
|
|
171
|
+
segments.push(entry.slice(cursor));
|
|
172
|
+
const result = segments.join("");
|
|
173
|
+
if (hadNestedBlock) {
|
|
174
|
+
return removeCompleteBootstrapBlocks(result);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
42
177
|
};
|
|
43
178
|
var applyBootstrapContent = (output, content) => {
|
|
44
179
|
for (let i = 0;i < output.system.length; i++) {
|
|
45
|
-
|
|
46
|
-
const block = findBootstrapMarkerBlock(entry);
|
|
47
|
-
if (block !== null) {
|
|
48
|
-
output.system[i] = entry.slice(0, block.start) + content + entry.slice(block.end);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
180
|
+
output.system[i] = removeCompleteBootstrapBlocks(output.system[i]);
|
|
51
181
|
}
|
|
52
|
-
if (output.system.length
|
|
53
|
-
output.system[output.system.length - 1] += `
|
|
54
|
-
|
|
55
|
-
${content}`;
|
|
56
|
-
} else {
|
|
182
|
+
if (output.system.length === 0) {
|
|
57
183
|
output.system.push(content);
|
|
184
|
+
return;
|
|
58
185
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return `**Tool Mapping for OpenCode:**
|
|
62
|
-
When skills reference tools you don't have, substitute OpenCode equivalents:
|
|
63
|
-
- \`TodoWrite\` \u2192 \`todowrite\`
|
|
64
|
-
- \`Task\` tool with subagents \u2192 Use OpenCode's subagent system (@mention)
|
|
65
|
-
- \`Skill\` tool \u2192 OpenCode's native \`skill\` tool
|
|
66
|
-
- \`SystematicSkill\` tool \u2192 \`systematic_skill\` (Systematic plugin skills)
|
|
67
|
-
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` \u2192 Your native tools
|
|
186
|
+
const first = output.system[0];
|
|
187
|
+
output.system[0] = first.length > 0 ? `${first}
|
|
68
188
|
|
|
69
|
-
|
|
70
|
-
|
|
189
|
+
${content}` : content;
|
|
190
|
+
};
|
|
191
|
+
function getSkillUsageTemplate() {
|
|
192
|
+
return `**Skills naming:**
|
|
193
|
+
- Systematic bundled skills use the \`systematic:\` prefix (e.g., \`systematic:setup\`)
|
|
194
|
+
- Workflow skills with their own namespace keep it (e.g., \`ce:brainstorm\`)
|
|
71
195
|
- Skills can also be invoked without prefix if unambiguous
|
|
72
196
|
|
|
73
197
|
**Skills usage:**
|
|
74
198
|
- Use \`systematic_skill\` to load Systematic bundled skills
|
|
75
|
-
- Use the
|
|
199
|
+
- Use the \`skill\` tool for non-Systematic skills
|
|
76
200
|
|
|
77
201
|
**Skills location:**
|
|
78
202
|
Bundled skills ship with the Systematic plugin and are discoverable via \`systematic_skill\`.`;
|
|
@@ -82,40 +206,47 @@ function getBootstrapContent(config, deps) {
|
|
|
82
206
|
if (!config.bootstrap.enabled)
|
|
83
207
|
return null;
|
|
84
208
|
if (config.bootstrap.file) {
|
|
85
|
-
const customPath = config.bootstrap.file.startsWith("~/") ?
|
|
209
|
+
const customPath = config.bootstrap.file.startsWith("~/") ? path2.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
|
|
86
210
|
if (fs.existsSync(customPath)) {
|
|
87
211
|
return fs.readFileSync(customPath, "utf8");
|
|
88
212
|
}
|
|
89
213
|
}
|
|
90
|
-
const usingSystematicPath =
|
|
214
|
+
const usingSystematicPath = path2.join(bundledSkillsDir, "using-systematic/SKILL.md");
|
|
91
215
|
if (!fs.existsSync(usingSystematicPath))
|
|
92
216
|
return null;
|
|
93
217
|
const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
|
|
94
218
|
const { body } = parseFrontmatter(fullContent);
|
|
95
219
|
const content = body.trim();
|
|
96
|
-
const
|
|
220
|
+
const skillUsage = getSkillUsageTemplate();
|
|
221
|
+
const catalog = renderCatalogVerbose({
|
|
222
|
+
bundledSkillsDir,
|
|
223
|
+
disabledSkills: config.disabled_skills
|
|
224
|
+
});
|
|
225
|
+
const catalogSection = catalog.length > 0 ? `
|
|
226
|
+
|
|
227
|
+
${catalog}` : "";
|
|
97
228
|
return `<SYSTEMATIC_WORKFLOWS>
|
|
98
|
-
You have access to structured engineering workflows via the
|
|
229
|
+
You have access to structured engineering workflows via the Systematic plugin.
|
|
99
230
|
|
|
100
231
|
**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.**
|
|
101
232
|
|
|
102
233
|
${content}
|
|
103
234
|
|
|
104
|
-
${
|
|
235
|
+
${skillUsage}${catalogSection}
|
|
105
236
|
</SYSTEMATIC_WORKFLOWS>`;
|
|
106
237
|
}
|
|
107
238
|
|
|
108
239
|
// src/lib/agent-overlays.ts
|
|
109
240
|
import fs2 from "fs";
|
|
110
|
-
import
|
|
241
|
+
import path4 from "path";
|
|
111
242
|
|
|
112
243
|
// src/lib/source-model-defaults.ts
|
|
113
|
-
import * as
|
|
244
|
+
import * as path3 from "path";
|
|
114
245
|
import { fileURLToPath } from "url";
|
|
115
246
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
116
|
-
var __dirname2 =
|
|
117
|
-
var packageRoot =
|
|
118
|
-
var bundledAgentsDir =
|
|
247
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
248
|
+
var packageRoot = path3.resolve(__dirname2, "..", "..");
|
|
249
|
+
var bundledAgentsDir = path3.join(packageRoot, "agents");
|
|
119
250
|
var ProviderID = exports_external.union([
|
|
120
251
|
exports_external.literal("vercel"),
|
|
121
252
|
exports_external.literal("opencode"),
|
|
@@ -325,14 +456,14 @@ function buildBundledAgentInventory(agentsDir, disabledAgents) {
|
|
|
325
456
|
const stemCategories = new Map;
|
|
326
457
|
const disabledSet = new Set(disabledAgents);
|
|
327
458
|
for (const category of categories) {
|
|
328
|
-
for (const fileName of readMarkdownFiles(
|
|
459
|
+
for (const fileName of readMarkdownFiles(path4.join(agentsDir, category))) {
|
|
329
460
|
const key = fileName.replace(/\.md$/, "");
|
|
330
461
|
const id = `${category}/${key}`;
|
|
331
462
|
agentsByQualifiedId[id] = {
|
|
332
463
|
id,
|
|
333
464
|
key,
|
|
334
465
|
category,
|
|
335
|
-
file:
|
|
466
|
+
file: path4.join(agentsDir, category, fileName),
|
|
336
467
|
disabled: disabledSet.has(key) || disabledSet.has(id)
|
|
337
468
|
};
|
|
338
469
|
const existing = stemCategories.get(key) ?? [];
|
|
@@ -496,7 +627,7 @@ function throwConfigError(sourcePath, keyPath, message) {
|
|
|
496
627
|
import { createHash } from "crypto";
|
|
497
628
|
import fs3 from "fs";
|
|
498
629
|
import os2 from "os";
|
|
499
|
-
import
|
|
630
|
+
import path5 from "path";
|
|
500
631
|
function emptyAvailability() {
|
|
501
632
|
return { status: "unknown", models: new Set };
|
|
502
633
|
}
|
|
@@ -505,8 +636,8 @@ var DEFAULT_API_TIMEOUT_MS = 1500;
|
|
|
505
636
|
var MODELS_JSON_FILENAME = "models.json";
|
|
506
637
|
function resolveCacheDir() {
|
|
507
638
|
const xdgCacheHome = process.env.XDG_CACHE_HOME?.trim();
|
|
508
|
-
const cacheBase = xdgCacheHome &&
|
|
509
|
-
return
|
|
639
|
+
const cacheBase = xdgCacheHome && path5.isAbsolute(xdgCacheHome) ? xdgCacheHome : path5.join(os2.homedir(), ".cache");
|
|
640
|
+
return path5.join(cacheBase, "opencode");
|
|
510
641
|
}
|
|
511
642
|
function fastHash(input) {
|
|
512
643
|
return createHash("sha1").update(input).digest("hex");
|
|
@@ -584,14 +715,14 @@ function readFallbackCache() {
|
|
|
584
715
|
const cacheDir = resolveCacheDir();
|
|
585
716
|
const openCodeModelsUrl = process.env.OPENCODE_MODELS_URL?.trim();
|
|
586
717
|
if (openCodeModelsUrl) {
|
|
587
|
-
const urlDerivedPath =
|
|
718
|
+
const urlDerivedPath = path5.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
|
|
588
719
|
const urlResult = readModelsFromCache(urlDerivedPath);
|
|
589
720
|
if (urlResult !== null) {
|
|
590
721
|
return { status: "cache", models: urlResult };
|
|
591
722
|
}
|
|
592
723
|
return emptyAvailability();
|
|
593
724
|
}
|
|
594
|
-
const defaultPath =
|
|
725
|
+
const defaultPath = path5.join(cacheDir, MODELS_JSON_FILENAME);
|
|
595
726
|
const defaultResult = readModelsFromCache(defaultPath);
|
|
596
727
|
if (defaultResult !== null) {
|
|
597
728
|
return { status: "cache", models: defaultResult };
|
|
@@ -642,66 +773,6 @@ async function getAvailableModels(client, options = {}) {
|
|
|
642
773
|
};
|
|
643
774
|
}
|
|
644
775
|
|
|
645
|
-
// src/lib/skill-loader.ts
|
|
646
|
-
import path5 from "path";
|
|
647
|
-
var SKILL_PREFIX = "systematic:";
|
|
648
|
-
var SKILL_DESCRIPTION_PREFIX = "(Systematic - Skill) ";
|
|
649
|
-
function formatSkillCommandName(name) {
|
|
650
|
-
if (name.includes(":")) {
|
|
651
|
-
return name;
|
|
652
|
-
}
|
|
653
|
-
return `${SKILL_PREFIX}${name}`;
|
|
654
|
-
}
|
|
655
|
-
function formatSkillDescription(description, fallbackName) {
|
|
656
|
-
const desc = description || `${fallbackName} skill`;
|
|
657
|
-
if (desc.startsWith(SKILL_DESCRIPTION_PREFIX)) {
|
|
658
|
-
return desc;
|
|
659
|
-
}
|
|
660
|
-
return `${SKILL_DESCRIPTION_PREFIX}${desc}`;
|
|
661
|
-
}
|
|
662
|
-
function wrapSkillTemplate(skillPath, body) {
|
|
663
|
-
const skillDir = path5.dirname(skillPath);
|
|
664
|
-
return `<skill-instruction>
|
|
665
|
-
Base directory for this skill: ${skillDir}/
|
|
666
|
-
File references (@path) in this skill are relative to this directory.
|
|
667
|
-
|
|
668
|
-
${body.trim()}
|
|
669
|
-
</skill-instruction>
|
|
670
|
-
|
|
671
|
-
<user-request>
|
|
672
|
-
$ARGUMENTS
|
|
673
|
-
</user-request>`;
|
|
674
|
-
}
|
|
675
|
-
function extractSkillBody(wrappedTemplate) {
|
|
676
|
-
const match = wrappedTemplate.match(/<skill-instruction>([\s\S]*?)<\/skill-instruction>/);
|
|
677
|
-
return match ? match[1].trim() : wrappedTemplate;
|
|
678
|
-
}
|
|
679
|
-
function loadSkill(skillInfo) {
|
|
680
|
-
try {
|
|
681
|
-
const converted = convertFileWithCache(skillInfo.skillFile, "skill", {
|
|
682
|
-
source: "bundled"
|
|
683
|
-
});
|
|
684
|
-
const { body } = parseFrontmatter(converted);
|
|
685
|
-
const wrappedTemplate = wrapSkillTemplate(skillInfo.skillFile, body);
|
|
686
|
-
return {
|
|
687
|
-
name: skillInfo.name,
|
|
688
|
-
prefixedName: formatSkillCommandName(skillInfo.name),
|
|
689
|
-
description: formatSkillDescription(skillInfo.description, skillInfo.name),
|
|
690
|
-
path: skillInfo.path,
|
|
691
|
-
skillFile: skillInfo.skillFile,
|
|
692
|
-
wrappedTemplate,
|
|
693
|
-
disableModelInvocation: skillInfo.disableModelInvocation,
|
|
694
|
-
userInvocable: skillInfo.userInvocable,
|
|
695
|
-
subtask: skillInfo.subtask,
|
|
696
|
-
agent: skillInfo.agent,
|
|
697
|
-
model: skillInfo.model,
|
|
698
|
-
argumentHint: skillInfo.argumentHint
|
|
699
|
-
};
|
|
700
|
-
} catch {
|
|
701
|
-
return null;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
776
|
// src/lib/config-handler.ts
|
|
706
777
|
function toTitleCase(name) {
|
|
707
778
|
return name.split("-").map((segment) => segment.length > 0 ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment).join("-");
|
|
@@ -1024,20 +1095,8 @@ function registerSkillsPaths(config, skillsDir) {
|
|
|
1024
1095
|
// src/lib/skill-tool.ts
|
|
1025
1096
|
import fs4 from "fs";
|
|
1026
1097
|
import path6 from "path";
|
|
1027
|
-
import { pathToFileURL } from "url";
|
|
1098
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1028
1099
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
1029
|
-
function formatSkillsXml(skills) {
|
|
1030
|
-
if (skills.length === 0)
|
|
1031
|
-
return "";
|
|
1032
|
-
const skillLines = skills.flatMap((skill) => [
|
|
1033
|
-
" <skill>",
|
|
1034
|
-
` <name>${formatSkillCommandName(skill.name)}</name>`,
|
|
1035
|
-
` <description>${skill.description}</description>`,
|
|
1036
|
-
` <location>${pathToFileURL(skill.path).href}</location>`,
|
|
1037
|
-
" </skill>"
|
|
1038
|
-
]);
|
|
1039
|
-
return ["<available_skills>", ...skillLines, "</available_skills>"].join(" ");
|
|
1040
|
-
}
|
|
1041
1100
|
function discoverSkillFiles(dir, limit = 10) {
|
|
1042
1101
|
const files = [];
|
|
1043
1102
|
function shouldSkipDirectory(name) {
|
|
@@ -1076,40 +1135,21 @@ function createSkillTool(options) {
|
|
|
1076
1135
|
const getAllSkills = () => {
|
|
1077
1136
|
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));
|
|
1078
1137
|
};
|
|
1079
|
-
const getDiscoverableSkills = () => {
|
|
1080
|
-
return getAllSkills().filter((s) => s.disableModelInvocation !== true);
|
|
1081
|
-
};
|
|
1082
1138
|
const buildDescription = () => {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const systematicXml = formatSkillsXml(skillInfos);
|
|
1094
|
-
return [
|
|
1095
|
-
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
|
1096
|
-
"",
|
|
1097
|
-
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
|
1098
|
-
"",
|
|
1099
|
-
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
|
|
1100
|
-
"",
|
|
1101
|
-
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
|
|
1102
|
-
"",
|
|
1103
|
-
"The following skills provide specialized sets of instructions for particular tasks.",
|
|
1104
|
-
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
|
1105
|
-
"",
|
|
1106
|
-
systematicXml
|
|
1107
|
-
].join(`
|
|
1108
|
-
`);
|
|
1139
|
+
const catalog = renderCatalogCompact({ bundledSkillsDir, disabledSkills });
|
|
1140
|
+
return `Load a specialized skill that provides domain-specific instructions and workflows.
|
|
1141
|
+
|
|
1142
|
+
When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.
|
|
1143
|
+
|
|
1144
|
+
The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.
|
|
1145
|
+
|
|
1146
|
+
Tool output includes a \`<skill_content name="...">\` block with the loaded content.
|
|
1147
|
+
|
|
1148
|
+
${catalog}`;
|
|
1109
1149
|
};
|
|
1110
1150
|
const buildParameterHint = () => {
|
|
1111
|
-
const
|
|
1112
|
-
const examples =
|
|
1151
|
+
const entries = buildCatalogEntries({ bundledSkillsDir, disabledSkills });
|
|
1152
|
+
const examples = entries.slice(0, 3).map((s) => `'${s.prefixedName}'`).join(", ");
|
|
1113
1153
|
const hint = examples.length > 0 ? ` (e.g., ${examples}, ...)` : "";
|
|
1114
1154
|
return `The name of the skill from available_skills${hint}`;
|
|
1115
1155
|
};
|
|
@@ -1136,12 +1176,15 @@ function createSkillTool(options) {
|
|
|
1136
1176
|
const skills = getAllSkills();
|
|
1137
1177
|
const matchedSkill = skills.find((s) => s.name === normalizedName);
|
|
1138
1178
|
if (!matchedSkill) {
|
|
1139
|
-
const availableSystematic =
|
|
1179
|
+
const availableSystematic = buildCatalogEntries({
|
|
1180
|
+
bundledSkillsDir,
|
|
1181
|
+
disabledSkills
|
|
1182
|
+
}).map((s) => s.prefixedName);
|
|
1140
1183
|
throw new Error(`Skill "${requestedName}" not found. Available systematic skills: ${availableSystematic.join(", ")}`);
|
|
1141
1184
|
}
|
|
1142
1185
|
const body = extractSkillBody(matchedSkill.wrappedTemplate);
|
|
1143
1186
|
const dir = path6.dirname(matchedSkill.skillFile);
|
|
1144
|
-
const base =
|
|
1187
|
+
const base = pathToFileURL2(dir).href;
|
|
1145
1188
|
const files = discoverSkillFiles(dir);
|
|
1146
1189
|
await context.ask({
|
|
1147
1190
|
permission: "skill",
|
package/dist/lib/bootstrap.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { SystematicConfig } from './config.js';
|
|
2
2
|
export declare const INTERNAL_AGENT_SIGNATURES: string[];
|
|
3
3
|
/**
|
|
4
|
-
* Inject bootstrap content into the system prompt array,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Inject bootstrap content into the system prompt array, placing exactly one
|
|
5
|
+
* canonical block at the end of `output.system[0]`.
|
|
6
|
+
*
|
|
7
|
+
* All complete `<SYSTEMATIC_WORKFLOWS>…</SYSTEMATIC_WORKFLOWS>` blocks are
|
|
8
|
+
* removed from every system entry first, then the current content is appended
|
|
9
|
+
* once to `output.system[0]`. Partial/malformed marker fragments are left
|
|
10
|
+
* untouched. This makes duplicate registration and prior last-entry placement
|
|
11
|
+
* converge to the new canonical location; later invocations win.
|
|
7
12
|
*
|
|
8
13
|
* Exported for test access — must NOT be re-exported from the plugin entry
|
|
9
14
|
* point (src/index.ts) because OpenCode's plugin loader expects a single
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape XML special characters (&, <, >) in text content.
|
|
3
|
+
* Quotes are not escaped because catalog names and descriptions are rendered
|
|
4
|
+
* as element text, not attribute values.
|
|
5
|
+
*/
|
|
6
|
+
export declare function escapeXml(text: string): string;
|
|
7
|
+
export interface CatalogEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
prefixedName: string;
|
|
10
|
+
description: string;
|
|
11
|
+
path: string;
|
|
12
|
+
skillFile: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CatalogOptions {
|
|
15
|
+
bundledSkillsDir: string;
|
|
16
|
+
disabledSkills: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Discovers and filters bundled Systematic skills into catalog entries.
|
|
20
|
+
* Excludes disabled skills and skills with disableModelInvocation === true.
|
|
21
|
+
* Returns entries sorted by name.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildCatalogEntries(options: CatalogOptions): CatalogEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* Renders discoverable skills as native-style verbose XML for bootstrap content.
|
|
26
|
+
* Returns empty string when no skills are available.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderCatalogVerbose(options: CatalogOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Renders discoverable skills as a compact markdown list for tool descriptions.
|
|
31
|
+
* Always includes the heading; renders an explicit no-skills message when empty.
|
|
32
|
+
*/
|
|
33
|
+
export declare function renderCatalogCompact(options: CatalogOptions): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fro.bot/systematic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "Structured engineering workflows for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://fro.bot/systematic",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@biomejs/biome": "2.4.15",
|
|
67
|
-
"@opencode-ai/plugin": "1.14.
|
|
68
|
-
"@opencode-ai/sdk": "1.14.
|
|
67
|
+
"@opencode-ai/plugin": "1.14.48",
|
|
68
|
+
"@opencode-ai/sdk": "1.14.48",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
70
|
"@types/js-yaml": "4.0.9",
|
|
71
71
|
"@types/node": "24.12.3",
|
|
@@ -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 `systematic_skill` tool for Systematic bundled skills. Use the
|
|
16
|
+
Use the `systematic_skill` tool for Systematic bundled skills. Use the `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
|
|
|
@@ -94,4 +94,4 @@ Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.
|
|
|
94
94
|
|
|
95
95
|
## Skill Resolution
|
|
96
96
|
|
|
97
|
-
Systematic bundled skills are listed in the `systematic_skill` tool description. Use the
|
|
97
|
+
Systematic bundled skills are listed in the `systematic_skill` tool description. Use the `skill` tool for skills outside the Systematic plugin.
|