@fro.bot/systematic 2.14.0 → 2.14.2

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
@@ -402,7 +402,7 @@ The plugin exposes one tool to OpenCode:
402
402
  |------|-------------|
403
403
  | `systematic_skill` | Load Systematic bundled skills by name. Lists available skills in its description and returns formatted skill content when invoked. |
404
404
 
405
- For non-Systematic skills (project or user-level), use OpenCode's native `skill` tool.
405
+ For non-Systematic skills (project or user-level), use OpenCode's `skill` tool.
406
406
 
407
407
  ## How It Works
408
408
 
package/dist/index.js CHANGED
@@ -188,22 +188,15 @@ var applyBootstrapContent = (output, content) => {
188
188
 
189
189
  ${content}` : content;
190
190
  };
191
- function getToolMappingTemplate() {
192
- return `**Tool Mapping for OpenCode:**
193
- When skills reference tools you don't have, substitute OpenCode equivalents:
194
- - \`TodoWrite\` \u2192 \`todowrite\`
195
- - \`Task\` tool with subagents \u2192 Use OpenCode's subagent system (@mention)
196
- - \`Skill\` tool \u2192 OpenCode's native \`skill\` tool
197
- - \`SystematicSkill\` tool \u2192 \`systematic_skill\` (Systematic plugin skills)
198
- - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` \u2192 Your native tools
199
-
200
- **Skills naming:**
201
- - Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
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\`)
202
195
  - Skills can also be invoked without prefix if unambiguous
203
196
 
204
197
  **Skills usage:**
205
198
  - Use \`systematic_skill\` to load Systematic bundled skills
206
- - Use the native \`skill\` tool for non-Systematic skills
199
+ - Use the \`skill\` tool for non-Systematic skills
207
200
 
208
201
  **Skills location:**
209
202
  Bundled skills ship with the Systematic plugin and are discoverable via \`systematic_skill\`.`;
@@ -224,7 +217,7 @@ function getBootstrapContent(config, deps) {
224
217
  const fullContent = fs.readFileSync(usingSystematicPath, "utf8");
225
218
  const { body } = parseFrontmatter(fullContent);
226
219
  const content = body.trim();
227
- const toolMapping = getToolMappingTemplate();
220
+ const skillUsage = getSkillUsageTemplate();
228
221
  const catalog = renderCatalogVerbose({
229
222
  bundledSkillsDir,
230
223
  disabledSkills: config.disabled_skills
@@ -233,13 +226,13 @@ function getBootstrapContent(config, deps) {
233
226
 
234
227
  ${catalog}` : "";
235
228
  return `<SYSTEMATIC_WORKFLOWS>
236
- You have access to structured engineering workflows via the systematic plugin.
229
+ You have access to structured engineering workflows via the Systematic plugin.
237
230
 
238
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.**
239
232
 
240
233
  ${content}
241
234
 
242
- ${toolMapping}${catalogSection}
235
+ ${skillUsage}${catalogSection}
243
236
  </SYSTEMATIC_WORKFLOWS>`;
244
237
  }
245
238
 
@@ -781,6 +774,28 @@ async function getAvailableModels(client, options = {}) {
781
774
  }
782
775
 
783
776
  // src/lib/config-handler.ts
777
+ function isSystematicAgentConfig(agent) {
778
+ const description = agent?.description;
779
+ return typeof description === "string" && /\(.* - Systematic\)$/.test(description);
780
+ }
781
+ function isSystematicCommandConfig(command) {
782
+ const description = command?.description;
783
+ return typeof description === "string" && (description.startsWith("(Systematic) ") || description.startsWith("(Systematic - Skill) "));
784
+ }
785
+ function mergeSystematicEntries(existing, emitted, shouldDropExisting) {
786
+ const merged = { ...existing ?? {} };
787
+ for (const [key, value] of Object.entries(existing ?? {})) {
788
+ if (shouldDropExisting(key, value)) {
789
+ delete merged[key];
790
+ }
791
+ }
792
+ for (const [key, value] of Object.entries(emitted)) {
793
+ if (Object.hasOwn(merged, key))
794
+ continue;
795
+ merged[key] = value;
796
+ }
797
+ return merged;
798
+ }
784
799
  function toTitleCase(name) {
785
800
  return name.split("-").map((segment) => segment.length > 0 ? segment.charAt(0).toUpperCase() + segment.slice(1) : segment).join("-");
786
801
  }
@@ -919,30 +934,37 @@ function applyAgentOverlays(config, agentInfo, overlays, availabilitySet) {
919
934
  addPermissionRules(permissionRules, config.permission);
920
935
  }
921
936
  result.temperature = inferBuiltInTemperature(agentInfo.name, result.description);
922
- if (agentInfo.category && availabilitySet !== undefined) {
923
- const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
924
- result.model = `${resolved.provider}/${resolved.model}`;
925
- if (resolved.variant !== undefined) {
926
- result.variant = resolved.variant;
927
- } else {
928
- delete result.variant;
929
- }
930
- }
931
- if (categoryOverlay) {
932
- applyOverlayObjectWithVariantClearing(result, categoryOverlay.value, permissionRules);
933
- }
934
- if (exactOverlay) {
935
- applyOverlayObjectWithVariantClearing(result, exactOverlay.value, permissionRules);
937
+ applySourceModelDefault(result, agentInfo, availabilitySet);
938
+ applyAgentOverlay(result, categoryOverlay?.value, permissionRules);
939
+ applyAgentOverlay(result, exactOverlay?.value, permissionRules);
940
+ applyPermissionOverlay(result, permissionRules, hasPermissionOverlay);
941
+ return result;
942
+ }
943
+ function applySourceModelDefault(target, agentInfo, availabilitySet) {
944
+ if (!agentInfo.category || availabilitySet === undefined)
945
+ return;
946
+ const resolved = resolveSourceModel(agentInfo.category, availabilitySet);
947
+ target.model = `${resolved.provider}/${resolved.model}`;
948
+ if (resolved.variant !== undefined) {
949
+ target.variant = resolved.variant;
950
+ } else {
951
+ delete target.variant;
936
952
  }
937
- if (hasPermissionOverlay) {
938
- const permission = permissionFromRules(permissionRules);
939
- if (permission) {
940
- result.permission = permission;
941
- } else {
942
- delete result.permission;
943
- }
953
+ }
954
+ function applyAgentOverlay(target, overlay, permissionRules) {
955
+ if (overlay === undefined)
956
+ return;
957
+ applyOverlayObjectWithVariantClearing(target, overlay, permissionRules);
958
+ }
959
+ function applyPermissionOverlay(target, permissionRules, hasPermissionOverlay) {
960
+ if (!hasPermissionOverlay)
961
+ return;
962
+ const permission = permissionFromRules(permissionRules);
963
+ if (permission) {
964
+ target.permission = permission;
965
+ } else {
966
+ delete target.permission;
944
967
  }
945
- return result;
946
968
  }
947
969
  function overlayControlsPermission(overlay) {
948
970
  return overlay !== undefined && (Object.hasOwn(overlay, "permission") || Object.hasOwn(overlay, "skills"));
@@ -1062,6 +1084,7 @@ function createConfigHandler(deps) {
1062
1084
  const { config: systematicConfig, overlays } = loadConfigWithSources(directory);
1063
1085
  const existingAgents = { ...config.agent ?? {} };
1064
1086
  const existingCommands = { ...config.command ?? {} };
1087
+ const nativeAgents = Object.fromEntries(Object.entries(existingAgents).filter(([, agent]) => !isSystematicAgentConfig(agent)));
1065
1088
  const bundledSkills = collectSkillsAsCommands(bundledSkillsDir, systematicConfig.disabled_skills);
1066
1089
  const enabledSkillNames = collectEnabledSkillNames(bundledSkillsDir, systematicConfig.disabled_skills);
1067
1090
  const inventory = buildBundledAgentInventory(bundledAgentsDir2, systematicConfig.disabled_agents);
@@ -1069,35 +1092,46 @@ function createConfigHandler(deps) {
1069
1092
  const validatedOverlays = validateAgentOverlays({
1070
1093
  inventory,
1071
1094
  overlays,
1072
- nativeAgents: existingAgents,
1095
+ nativeAgents,
1073
1096
  enabledSkills: enabledSkillNames
1074
1097
  });
1075
1098
  const resolvedOverlays = resolveAgentOverlaySet(validatedOverlays);
1076
1099
  const availability = deps.client ? await getAvailableModels(deps.client) : undefined;
1077
1100
  const availabilitySet = availability && availability.status !== "unknown" ? availability.models : undefined;
1078
- const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, existingAgents, resolvedOverlays, availabilitySet);
1101
+ const bundledAgents = collectAgents(bundledAgentsDir2, systematicConfig.disabled_agents, nativeAgents, resolvedOverlays, availabilitySet);
1079
1102
  const bundledCommands = collectCommands(bundledCommandsDir, systematicConfig.disabled_commands);
1080
- config.agent = {
1081
- ...bundledAgents,
1082
- ...existingAgents
1083
- };
1084
- config.command = {
1085
- ...bundledCommands,
1086
- ...bundledSkills,
1087
- ...existingCommands
1088
- };
1103
+ const bundledAgentKeys = new Set(Object.keys(bundledAgents));
1104
+ config.agent = mergeSystematicEntries(existingAgents, bundledAgents, (key, agent) => bundledAgentKeys.has(key) && isSystematicAgentConfig(agent));
1105
+ const emittedCommands = { ...bundledCommands, ...bundledSkills };
1106
+ const emittedCommandKeys = new Set(Object.keys(emittedCommands));
1107
+ config.command = mergeSystematicEntries(existingCommands, emittedCommands, (key, command) => isSystematicCommandConfig(command) && (emittedCommandKeys.has(key) || isSystematicOwnedCommandKey(key)));
1089
1108
  registerSkillsPaths(config, bundledSkillsDir);
1090
1109
  };
1091
1110
  }
1092
1111
  function registerSkillsPaths(config, skillsDir) {
1093
1112
  const extended = config;
1094
1113
  const paths = extended.skills?.paths ?? [];
1095
- const nextPaths = paths.includes(skillsDir) ? [...paths] : [...paths, skillsDir];
1114
+ const nextPaths = removeSystematicSkillPaths(paths);
1115
+ if (!nextPaths.includes(skillsDir))
1116
+ nextPaths.push(skillsDir);
1096
1117
  extended.skills = {
1097
1118
  ...extended.skills,
1098
1119
  paths: nextPaths
1099
1120
  };
1100
1121
  }
1122
+ function removeSystematicSkillPaths(paths) {
1123
+ return paths.filter((path6) => !isSystematicSkillPath(path6));
1124
+ }
1125
+ function isSystematicSkillPath(path6) {
1126
+ const normalizedPath = normalizePath(path6);
1127
+ return normalizedPath.endsWith("/.config/opencode/systematic/skills") || normalizedPath.endsWith("/.cache/opencode/systematic/skills") || normalizedPath.endsWith("/.local/share/opencode/systematic/skills") || normalizedPath.endsWith("/.opencode/systematic/skills") || /(?:^|\/)\.cache\/opencode\/packages\/@fro\.bot\/systematic@[^/]+\/node_modules\/@fro\.bot\/systematic\/skills(?:$|\/)/u.test(normalizedPath);
1128
+ }
1129
+ function normalizePath(path6) {
1130
+ return path6.replaceAll("\\", "/").replace(/\/+$/u, "");
1131
+ }
1132
+ function isSystematicOwnedCommandKey(key) {
1133
+ return key.startsWith("systematic:") || key.startsWith("ce:");
1134
+ }
1101
1135
 
1102
1136
  // src/lib/skill-tool.ts
1103
1137
  import fs4 from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "2.14.0",
3
+ "version": "2.14.2",
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.47",
68
- "@opencode-ai/sdk": "1.14.47",
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.