@sweny-ai/core 0.1.78 → 0.1.80

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/executor.js CHANGED
@@ -17,6 +17,7 @@ import { resolveSources } from "./source-resolver.js";
17
17
  import { evaluateVerify } from "./verify.js";
18
18
  import { evaluateRequires } from "./requires.js";
19
19
  import { buildRetryPreamble } from "./retry.js";
20
+ import { buildToolAliases } from "./skills/index.js";
20
21
  /**
21
22
  * Execute a workflow from entry to completion.
22
23
  *
@@ -37,6 +38,10 @@ export async function execute(workflow, input, options) {
37
38
  const nodeRunCounts = new Map(); // node → times executed
38
39
  const trace = { steps: [], edges: [], sources: {} };
39
40
  validate(workflow, skills);
41
+ // Build a verify-time alias table from the loaded skills. Each skill owns
42
+ // its own mapping between skill-tool names and equivalent MCP names. Core
43
+ // stays vendor-neutral — this call just unions what the skills declare.
44
+ const toolAliases = buildToolAliases(skills.values(), logger);
40
45
  // ── Source resolution phase ─────────────────────────────────────
41
46
  // Resolve all Source values (node instructions + rules/context at every
42
47
  // level) into plain text before the main execution loop. This eagerly
@@ -159,7 +164,7 @@ export async function execute(workflow, input, options) {
159
164
  // Retry only triggers on verify failure — bail on tool/API errors.
160
165
  if (result.status !== "success")
161
166
  break;
162
- const verifyError = evaluateVerify(node.verify, result);
167
+ const verifyError = evaluateVerify(node.verify, result, toolAliases);
163
168
  if (!verifyError)
164
169
  break;
165
170
  // Apply verify failure to the result.
package/dist/schema.d.ts CHANGED
@@ -236,6 +236,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
236
236
  command?: string | undefined;
237
237
  args?: string[] | undefined;
238
238
  }>>;
239
+ mcpAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
239
240
  }, "strip", z.ZodTypeAny, {
240
241
  id: string;
241
242
  name: string;
@@ -260,6 +261,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
260
261
  command?: string | undefined;
261
262
  args?: string[] | undefined;
262
263
  } | undefined;
264
+ mcpAliases?: Record<string, string[]> | undefined;
263
265
  }, {
264
266
  id: string;
265
267
  name: string;
@@ -284,6 +286,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
284
286
  description: string;
285
287
  input_schema: Record<string, unknown>;
286
288
  }[] | undefined;
289
+ mcpAliases?: Record<string, string[]> | undefined;
287
290
  }>, {
288
291
  id: string;
289
292
  name: string;
@@ -308,6 +311,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
308
311
  command?: string | undefined;
309
312
  args?: string[] | undefined;
310
313
  } | undefined;
314
+ mcpAliases?: Record<string, string[]> | undefined;
311
315
  }, {
312
316
  id: string;
313
317
  name: string;
@@ -332,6 +336,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
332
336
  description: string;
333
337
  input_schema: Record<string, unknown>;
334
338
  }[] | undefined;
339
+ mcpAliases?: Record<string, string[]> | undefined;
335
340
  }>;
336
341
  /**
337
342
  * NodeSources: either an array of Sources (additive — inherits cascade)
package/dist/schema.js CHANGED
@@ -61,6 +61,7 @@ export const skillZ = z
61
61
  tools: z.array(toolZ).default([]),
62
62
  instruction: z.string().optional(),
63
63
  mcp: mcpServerConfigZ.optional(),
64
+ mcpAliases: z.record(z.array(z.string().min(1)).min(1)).optional(),
64
65
  })
65
66
  .refine((s) => s.tools.length > 0 || s.instruction || s.mcp, {
66
67
  message: "Skill must provide at least one of: tools, instruction, or mcp",
@@ -183,4 +183,21 @@ export const github = {
183
183
  },
184
184
  },
185
185
  ],
186
+ // Equivalent tool names on GitHub's official MCP server
187
+ // (github.com/github/github-mcp-server). Aliases declared for every
188
+ // GitHub MCP name that does NOT also exist on Linear's MCP. The
189
+ // executor's buildToolAliases already drops any name claimed by more
190
+ // than one loaded skill, so these declarations are safe even if a
191
+ // future provider starts using the same name — declare what this
192
+ // skill knows, let the runtime resolve conflicts.
193
+ //
194
+ // Names that currently exist on both Linear and GitHub MCPs and are
195
+ // therefore intentionally NOT aliased on either side: `get_issue`,
196
+ // `list_issues`.
197
+ mcpAliases: {
198
+ github_create_pr: ["create_pull_request"],
199
+ github_add_comment: ["add_issue_comment"],
200
+ github_create_issue: ["create_issue"],
201
+ github_search_issues: ["search_issues"],
202
+ },
186
203
  };
@@ -7,7 +7,7 @@
7
7
  * Custom skill discovery (filesystem-based) lives in `./custom-loader.js`,
8
8
  * which is Node-only and re-exported from `@sweny-ai/core` (the Node entry).
9
9
  */
10
- import type { Skill, SkillCategory } from "../types.js";
10
+ import type { Logger, Skill, SkillCategory } from "../types.js";
11
11
  import { github } from "./github.js";
12
12
  import { linear } from "./linear.js";
13
13
  import { slack } from "./slack.js";
@@ -86,3 +86,24 @@ export declare function validateWorkflowSkills(workflow: {
86
86
  skills: string[];
87
87
  }>;
88
88
  }, available: Map<string, Skill>, inlineSkills?: Record<string, InlineSkillDef>): SkillValidationResult;
89
+ /**
90
+ * Build a symmetric tool-name alias table from the loaded skills.
91
+ *
92
+ * Each skill declares its own `mcpAliases` (canonical name → list of
93
+ * equivalent MCP tool names). This function:
94
+ *
95
+ * 1. Collects every alias pair declared by every loaded skill.
96
+ * 2. Flattens each pair into one equivalence group (symmetric closure).
97
+ * 3. Drops any MCP name claimed by more than one skill (ambiguous).
98
+ *
99
+ * Core stays vendor-neutral: nothing here knows Linear or GitHub by name.
100
+ * Skills own their own naming, and ambiguity is detected at map-build
101
+ * time so a call to one provider's `get_issue` can never spuriously
102
+ * satisfy another provider's `any_tool_called` verify rule.
103
+ *
104
+ * @returns a map from every participating name to the full set of names
105
+ * that count as equivalent to it (including itself). Names that do not
106
+ * participate in any alias are absent — callers should fall back to
107
+ * name-equality when a lookup returns `undefined`.
108
+ */
109
+ export declare function buildToolAliases(skills: Iterable<Skill>, logger?: Logger): ReadonlyMap<string, ReadonlySet<string>>;
@@ -144,3 +144,94 @@ export function validateWorkflowSkills(workflow, available, inlineSkills) {
144
144
  }
145
145
  return { configured, missing, errors, warnings };
146
146
  }
147
+ /**
148
+ * Build a symmetric tool-name alias table from the loaded skills.
149
+ *
150
+ * Each skill declares its own `mcpAliases` (canonical name → list of
151
+ * equivalent MCP tool names). This function:
152
+ *
153
+ * 1. Collects every alias pair declared by every loaded skill.
154
+ * 2. Flattens each pair into one equivalence group (symmetric closure).
155
+ * 3. Drops any MCP name claimed by more than one skill (ambiguous).
156
+ *
157
+ * Core stays vendor-neutral: nothing here knows Linear or GitHub by name.
158
+ * Skills own their own naming, and ambiguity is detected at map-build
159
+ * time so a call to one provider's `get_issue` can never spuriously
160
+ * satisfy another provider's `any_tool_called` verify rule.
161
+ *
162
+ * @returns a map from every participating name to the full set of names
163
+ * that count as equivalent to it (including itself). Names that do not
164
+ * participate in any alias are absent — callers should fall back to
165
+ * name-equality when a lookup returns `undefined`.
166
+ */
167
+ export function buildToolAliases(skills, logger) {
168
+ // First pass: record which skill(s) claim each MCP alias name.
169
+ const mcpClaims = new Map(); // mcp name → skill ids that declared it
170
+ const pairs = []; // [canonical, mcpAlias]
171
+ for (const skill of skills) {
172
+ if (!skill.mcpAliases)
173
+ continue;
174
+ for (const [canonical, aliases] of Object.entries(skill.mcpAliases)) {
175
+ for (const alias of aliases) {
176
+ if (canonical === alias)
177
+ continue;
178
+ pairs.push([canonical, alias]);
179
+ const claimers = mcpClaims.get(alias);
180
+ if (claimers) {
181
+ if (!claimers.includes(skill.id))
182
+ claimers.push(skill.id);
183
+ }
184
+ else {
185
+ mcpClaims.set(alias, [skill.id]);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ // Drop ambiguous MCP names (claimed by >1 skill) and log once.
191
+ const ambiguous = new Set();
192
+ for (const [name, claimers] of mcpClaims) {
193
+ if (claimers.length > 1) {
194
+ ambiguous.add(name);
195
+ logger?.warn(`Tool alias "${name}" is declared by multiple skills (${claimers.join(", ")}); dropping from verify alias table to avoid cross-provider false positives.`);
196
+ }
197
+ }
198
+ // Second pass: union-find over the remaining pairs.
199
+ const parent = new Map();
200
+ const find = (x) => {
201
+ let cur = parent.get(x) ?? x;
202
+ while (cur !== (parent.get(cur) ?? cur))
203
+ cur = parent.get(cur) ?? cur;
204
+ parent.set(x, cur);
205
+ return cur;
206
+ };
207
+ const union = (a, b) => {
208
+ const ra = find(a);
209
+ const rb = find(b);
210
+ if (ra !== rb)
211
+ parent.set(ra, rb);
212
+ };
213
+ // `find` registers previously-unseen nodes via `parent.get(x) ?? x` and
214
+ // writes them back, so `union` handles first-contact pairs on its own.
215
+ for (const [canonical, alias] of pairs) {
216
+ if (ambiguous.has(alias))
217
+ continue;
218
+ union(canonical, alias);
219
+ }
220
+ // Build the result: each name → its full equivalence set.
221
+ const groups = new Map();
222
+ for (const name of parent.keys()) {
223
+ const root = find(name);
224
+ let group = groups.get(root);
225
+ if (!group) {
226
+ group = new Set();
227
+ groups.set(root, group);
228
+ }
229
+ group.add(name);
230
+ }
231
+ const result = new Map();
232
+ for (const group of groups.values()) {
233
+ for (const name of group)
234
+ result.set(name, group);
235
+ }
236
+ return result;
237
+ }
@@ -144,4 +144,24 @@ export const linear = {
144
144
  },
145
145
  },
146
146
  ],
147
+ // Equivalent tool names on Linear's official remote MCP server (mcp.linear.app).
148
+ //
149
+ // Linear's MCP uses upsert naming: a single `save_issue` handles both
150
+ // create and update. The executor builds symmetric equivalence groups, so
151
+ // both `linear_create_issue` and `linear_update_issue` end up in one group
152
+ // with `save_issue`. A `save_issue` call satisfies verify rules for either
153
+ // canonical name — verify cannot distinguish create from update when the
154
+ // agent uses the remote MCP. That reflects the provider's reality, not a
155
+ // shortcoming of the alias layer.
156
+ //
157
+ // Names that also exist on GitHub's MCP server (`get_issue`, `list_issues`,
158
+ // `get_team`) are left out. `buildToolAliases` also drops any MCP name
159
+ // claimed by more than one loaded skill as a defensive backstop.
160
+ mcpAliases: {
161
+ linear_create_issue: ["save_issue"],
162
+ linear_update_issue: ["save_issue"],
163
+ linear_search_issues: ["list_issues"],
164
+ linear_add_comment: ["save_comment"],
165
+ linear_list_teams: ["list_teams"],
166
+ },
147
167
  };
package/dist/types.d.ts CHANGED
@@ -36,6 +36,25 @@ export interface Skill {
36
36
  instruction?: string;
37
37
  /** External MCP server definition wired for nodes referencing this skill. */
38
38
  mcp?: McpServerConfig;
39
+ /**
40
+ * Tool-name aliases recognized by `verify`.
41
+ *
42
+ * When a workflow references `any_tool_called: [linear_create_issue]` and
43
+ * the agent instead calls the equivalent MCP tool exposed by this skill's
44
+ * MCP server (e.g. `save_issue` on Linear's remote MCP), the two names
45
+ * should count as equivalent. Each skill owns the mapping for its own
46
+ * domain — core stays vendor-neutral.
47
+ *
48
+ * Key: a canonical tool name (usually one of this skill's `tools[].name`,
49
+ * but any name the skill wants to equate is valid).
50
+ * Value: list of equivalent names, typically tool names exposed by this
51
+ * skill's external MCP server.
52
+ *
53
+ * Aliases are symmetric: a verify rule naming either side matches a call
54
+ * on either side. Omit names that are ambiguous across providers
55
+ * (e.g. `get_issue` is exposed by both Linear and GitHub MCP servers).
56
+ */
57
+ mcpAliases?: Record<string, string[]>;
39
58
  }
40
59
  /**
41
60
  * Per-node rules or context.
package/dist/verify.d.ts CHANGED
@@ -9,9 +9,10 @@ export type Resolution = {
9
9
  };
10
10
  export declare function resolvePath(data: unknown, path: string): Resolution;
11
11
  export declare function isErrorOutput(output: unknown): boolean;
12
- export declare function checkAnyToolCalled(required: string[], toolCalls: ToolCall[]): string | null;
13
- export declare function checkAllToolsCalled(required: string[], toolCalls: ToolCall[]): string | null;
14
- export declare function checkNoToolCalled(forbidden: string[], toolCalls: ToolCall[]): string | null;
12
+ export type ToolAliases = ReadonlyMap<string, ReadonlySet<string>>;
13
+ export declare function checkAnyToolCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
14
+ export declare function checkAllToolsCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
15
+ export declare function checkNoToolCalled(forbidden: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
15
16
  export declare function checkOutputRequired(paths: string[], data: unknown): string | null;
16
17
  export declare function checkOutputMatches(matches: OutputMatch[], data: unknown): string | null;
17
18
  /**
@@ -21,4 +22,4 @@ export declare function checkOutputMatches(matches: OutputMatch[], data: unknown
21
22
  * Deterministic and side-effect free — the executor calls this synchronously
22
23
  * after the LLM finishes.
23
24
  */
24
- export declare function evaluateVerify(verify: NodeVerify | undefined, result: NodeResult): string | null;
25
+ export declare function evaluateVerify(verify: NodeVerify | undefined, result: NodeResult, aliases?: ToolAliases): string | null;
package/dist/verify.js CHANGED
@@ -105,22 +105,32 @@ function succeededTools(toolCalls) {
105
105
  function calledList(toolCalls) {
106
106
  return toolCalls.map((c) => c.tool).join(", ") || "none";
107
107
  }
108
- export function checkAnyToolCalled(required, toolCalls) {
108
+ function expandAliases(name, aliases) {
109
+ return aliases?.get(name) ?? new Set([name]);
110
+ }
111
+ function anyAliasIn(name, set, aliases) {
112
+ for (const alias of expandAliases(name, aliases)) {
113
+ if (set.has(alias))
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ export function checkAnyToolCalled(required, toolCalls, aliases) {
109
119
  const succeeded = succeededTools(toolCalls);
110
- if (required.some((t) => succeeded.has(t)))
120
+ if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
111
121
  return null;
112
122
  return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
113
123
  }
114
- export function checkAllToolsCalled(required, toolCalls) {
124
+ export function checkAllToolsCalled(required, toolCalls, aliases) {
115
125
  const succeeded = succeededTools(toolCalls);
116
- const missing = required.filter((t) => !succeeded.has(t));
126
+ const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
117
127
  if (missing.length === 0)
118
128
  return null;
119
129
  return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
120
130
  }
121
- export function checkNoToolCalled(forbidden, toolCalls) {
131
+ export function checkNoToolCalled(forbidden, toolCalls, aliases) {
122
132
  const calledNames = new Set(toolCalls.map((c) => c.tool));
123
- const violated = forbidden.filter((t) => calledNames.has(t));
133
+ const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
124
134
  if (violated.length === 0)
125
135
  return null;
126
136
  return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
@@ -312,22 +322,22 @@ function describeOperator(c) {
312
322
  * Deterministic and side-effect free — the executor calls this synchronously
313
323
  * after the LLM finishes.
314
324
  */
315
- export function evaluateVerify(verify, result) {
325
+ export function evaluateVerify(verify, result, aliases) {
316
326
  if (!verify)
317
327
  return null;
318
328
  const failures = [];
319
329
  if (verify.any_tool_called && verify.any_tool_called.length > 0) {
320
- const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls);
330
+ const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls, aliases);
321
331
  if (e)
322
332
  failures.push(e);
323
333
  }
324
334
  if (verify.all_tools_called && verify.all_tools_called.length > 0) {
325
- const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls);
335
+ const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls, aliases);
326
336
  if (e)
327
337
  failures.push(e);
328
338
  }
329
339
  if (verify.no_tool_called && verify.no_tool_called.length > 0) {
330
- const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls);
340
+ const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls, aliases);
331
341
  if (e)
332
342
  failures.push(e);
333
343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.78",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"