@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/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  loadConfig,
14
14
  loadConfigWithSources,
15
15
  parseFrontmatter
16
- } from "./index-3xnyeknn.js";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- const entry = output.system[i];
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 > 0) {
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
- function getToolMappingTemplate() {
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
- **Skills naming:**
70
- - Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
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 native \`skill\` tool for non-Systematic skills
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("~/") ? path.join(os.homedir(), config.bootstrap.file.slice(2)) : config.bootstrap.file;
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 = path.join(bundledSkillsDir, "using-systematic/SKILL.md");
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 toolMapping = getToolMappingTemplate();
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 systematic plugin.
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
- ${toolMapping}
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 path3 from "path";
241
+ import path4 from "path";
111
242
 
112
243
  // src/lib/source-model-defaults.ts
113
- import * as path2 from "path";
244
+ import * as path3 from "path";
114
245
  import { fileURLToPath } from "url";
115
246
  var __filename2 = fileURLToPath(import.meta.url);
116
- var __dirname2 = path2.dirname(__filename2);
117
- var packageRoot = path2.resolve(__dirname2, "..", "..");
118
- var bundledAgentsDir = path2.join(packageRoot, "agents");
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(path3.join(agentsDir, category))) {
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: path3.join(agentsDir, category, fileName),
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 path4 from "path";
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 && path4.isAbsolute(xdgCacheHome) ? xdgCacheHome : path4.join(os2.homedir(), ".cache");
509
- return path4.join(cacheBase, "opencode");
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 = path4.join(cacheDir, `models-${fastHash(openCodeModelsUrl)}.json`);
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 = path4.join(cacheDir, MODELS_JSON_FILENAME);
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 skills = getDiscoverableSkills();
1084
- if (skills.length === 0) {
1085
- return "Load a skill to get detailed instructions for a specific task. No skills are currently available.";
1086
- }
1087
- const skillInfos = skills.map((s) => ({
1088
- name: s.name,
1089
- description: s.description,
1090
- path: s.path,
1091
- skillFile: s.skillFile
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 skills = getDiscoverableSkills();
1112
- const examples = skills.slice(0, 3).map((s) => `'${s.prefixedName}'`).join(", ");
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 = getDiscoverableSkills().map((s) => s.prefixedName);
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 = pathToFileURL(dir).href;
1187
+ const base = pathToFileURL2(dir).href;
1145
1188
  const files = discoverSkillFiles(dir);
1146
1189
  await context.ask({
1147
1190
  permission: "skill",
@@ -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, replacing any
5
- * existing `<SYSTEMATIC_WORKFLOWS>` block. Multi-load idempotency is via
6
- * the marker — most-recently-registered plugin wins under FIFO hook order.
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.13.3",
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.46",
68
- "@opencode-ai/sdk": "1.14.46",
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 native `skill` tool for non-Systematic skills. 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 `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 native `skill` tool for skills outside the Systematic plugin.
97
+ Systematic bundled skills are listed in the `systematic_skill` tool description. Use the `skill` tool for skills outside the Systematic plugin.