@sweny-ai/core 0.1.79 → 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,73 +105,32 @@ function succeededTools(toolCalls) {
105
105
  function calledList(toolCalls) {
106
106
  return toolCalls.map((c) => c.tool).join(", ") || "none";
107
107
  }
108
- // Tool aliases: first-party SWEny skill tool names ↔ their MCP-server equivalents.
109
- //
110
- // When an external MCP server is wired for the same provider a skill covers
111
- // (e.g. the Linear HTTP MCP server alongside the `linear` skill), both sets of
112
- // tools are presented to the agent and the agent may pick either. The calls
113
- // are functionally equivalent — same backend, same effect — but the names
114
- // differ, so a verify rule that lists one will fail when the agent picked the
115
- // other. These aliases let either call satisfy the rule.
116
- //
117
- // Only unambiguous MCP tool names are aliased. Names that collide across
118
- // providers (e.g. `get_issue` exists on both Linear and GitHub MCP servers)
119
- // are deliberately omitted so a call to one provider cannot spuriously
120
- // satisfy a rule about the other.
121
- const TOOL_ALIAS_GROUPS = [
122
- // Linear (official remote MCP at mcp.linear.app — upsert naming)
123
- ["linear_search_issues", "list_issues"],
124
- ["linear_create_issue", "save_issue"],
125
- ["linear_update_issue", "save_issue"],
126
- ["linear_add_comment", "save_comment"],
127
- ["linear_list_teams", "list_teams"],
128
- // GitHub (github.com/github/github-mcp-server)
129
- ["github_create_issue", "create_issue"],
130
- ["github_search_issues", "search_issues"],
131
- ["github_add_comment", "add_issue_comment"],
132
- ["github_create_pr", "create_pull_request"],
133
- ];
134
- const TOOL_ALIASES = (() => {
135
- const map = new Map();
136
- for (const group of TOOL_ALIAS_GROUPS) {
137
- for (const name of group) {
138
- let set = map.get(name);
139
- if (!set) {
140
- set = new Set();
141
- map.set(name, set);
142
- }
143
- for (const other of group)
144
- set.add(other);
145
- }
146
- }
147
- return map;
148
- })();
149
- function expandAliases(name) {
150
- return TOOL_ALIASES.get(name) ?? new Set([name]);
108
+ function expandAliases(name, aliases) {
109
+ return aliases?.get(name) ?? new Set([name]);
151
110
  }
152
- function anyAliasIn(name, set) {
153
- for (const alias of expandAliases(name)) {
111
+ function anyAliasIn(name, set, aliases) {
112
+ for (const alias of expandAliases(name, aliases)) {
154
113
  if (set.has(alias))
155
114
  return true;
156
115
  }
157
116
  return false;
158
117
  }
159
- export function checkAnyToolCalled(required, toolCalls) {
118
+ export function checkAnyToolCalled(required, toolCalls, aliases) {
160
119
  const succeeded = succeededTools(toolCalls);
161
- if (required.some((t) => anyAliasIn(t, succeeded)))
120
+ if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
162
121
  return null;
163
122
  return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
164
123
  }
165
- export function checkAllToolsCalled(required, toolCalls) {
124
+ export function checkAllToolsCalled(required, toolCalls, aliases) {
166
125
  const succeeded = succeededTools(toolCalls);
167
- const missing = required.filter((t) => !anyAliasIn(t, succeeded));
126
+ const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
168
127
  if (missing.length === 0)
169
128
  return null;
170
129
  return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
171
130
  }
172
- export function checkNoToolCalled(forbidden, toolCalls) {
131
+ export function checkNoToolCalled(forbidden, toolCalls, aliases) {
173
132
  const calledNames = new Set(toolCalls.map((c) => c.tool));
174
- const violated = forbidden.filter((t) => anyAliasIn(t, calledNames));
133
+ const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
175
134
  if (violated.length === 0)
176
135
  return null;
177
136
  return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
@@ -363,22 +322,22 @@ function describeOperator(c) {
363
322
  * Deterministic and side-effect free — the executor calls this synchronously
364
323
  * after the LLM finishes.
365
324
  */
366
- export function evaluateVerify(verify, result) {
325
+ export function evaluateVerify(verify, result, aliases) {
367
326
  if (!verify)
368
327
  return null;
369
328
  const failures = [];
370
329
  if (verify.any_tool_called && verify.any_tool_called.length > 0) {
371
- const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls);
330
+ const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls, aliases);
372
331
  if (e)
373
332
  failures.push(e);
374
333
  }
375
334
  if (verify.all_tools_called && verify.all_tools_called.length > 0) {
376
- const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls);
335
+ const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls, aliases);
377
336
  if (e)
378
337
  failures.push(e);
379
338
  }
380
339
  if (verify.no_tool_called && verify.no_tool_called.length > 0) {
381
- const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls);
340
+ const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls, aliases);
382
341
  if (e)
383
342
  failures.push(e);
384
343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"