@sweny-ai/core 0.1.125 → 0.1.127

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.
@@ -58,11 +58,48 @@ export interface BuildAgentEnvOpts {
58
58
  export declare function resolveEnvScope(env: Record<string, string | undefined>, explicit?: boolean, logger?: Pick<Logger, "warn">): boolean;
59
59
  /** Max names listed in the withheld-vars warning before "and N more". */
60
60
  export declare const WITHHELD_WARNING_CAP = 30;
61
+ /**
62
+ * Runner baseline: exact names set by common CI images (GitHub-hosted runner
63
+ * image, Actions runner context, Debian-based containers). They are present on
64
+ * every run, so listing them as "withheld" is noise. Names only; none of these
65
+ * is a credential the operator supplied. Secrets such as `GITHUB_TOKEN`,
66
+ * `NPM_TOKEN`, or `AWS_*` are deliberately absent, so withholding them still
67
+ * warns.
68
+ */
69
+ export declare const RUNNER_BASELINE_VARS: ReadonlySet<string>;
70
+ /**
71
+ * Runner baseline: name patterns for families the CI images set in bulk
72
+ * (Actions runner internals, Android/Java/.NET/Go/Python toolchains, browsers
73
+ * and webdrivers, Azure CLI, vcpkg, ghcup, ...). `AWS_*`, `NPM_*` and
74
+ * `GITHUB_TOKEN`-style names are intentionally not matched.
75
+ */
76
+ export declare const RUNNER_BASELINE_PATTERNS: readonly RegExp[];
77
+ /** True when `name` is set by the CI image itself, not by the workflow author. */
78
+ export declare function isRunnerBaselineVar(name: string): boolean;
79
+ /** Split withheld names into the runner baseline and everything else. */
80
+ export declare function classifyWithheld(withheld: readonly string[]): {
81
+ baseline: string[];
82
+ other: string[];
83
+ };
61
84
  /**
62
85
  * One-line warning naming the withheld variables. Names only, never values;
63
86
  * sorted; capped at {@link WITHHELD_WARNING_CAP}.
64
87
  */
65
88
  export declare function formatWithheldWarning(withheld: readonly string[]): string;
89
+ /** Plain-log summary: counts only, no names. */
90
+ export declare function formatScopeSummary(total: number, baseline: number): string;
91
+ /** Test seam: forget that the withheld notice was already emitted. */
92
+ export declare function resetWithheldReport(): void;
93
+ /**
94
+ * Report withheld env names, once per process (not per node or per client).
95
+ *
96
+ * - always: one plain `info` line with counts (not a `::warning::` annotation);
97
+ * - only when non-baseline names were withheld: a `warn` listing just those
98
+ * (capped), as a `::warning title=SWEny agent env::` annotation under GitHub
99
+ * Actions;
100
+ * - `debug` (shown with `--verbose`): every withheld name.
101
+ */
102
+ export declare function reportWithheldEnv(withheld: readonly string[], logger: Pick<Logger, "info" | "warn" | "debug">, env?: Record<string, string | undefined>): void;
66
103
  /**
67
104
  * Scope the agent env and report which variable names were withheld.
68
105
  * `withheld` holds names only; values never leave `source`.
package/dist/agent-env.js CHANGED
@@ -159,6 +159,100 @@ export function resolveEnvScope(env, explicit, logger) {
159
159
  }
160
160
  /** Max names listed in the withheld-vars warning before "and N more". */
161
161
  export const WITHHELD_WARNING_CAP = 30;
162
+ /**
163
+ * Runner baseline: exact names set by common CI images (GitHub-hosted runner
164
+ * image, Actions runner context, Debian-based containers). They are present on
165
+ * every run, so listing them as "withheld" is noise. Names only; none of these
166
+ * is a credential the operator supplied. Secrets such as `GITHUB_TOKEN`,
167
+ * `NPM_TOKEN`, or `AWS_*` are deliberately absent, so withholding them still
168
+ * warns.
169
+ */
170
+ export const RUNNER_BASELINE_VARS = new Set([
171
+ // image / OS
172
+ "ImageOS",
173
+ "ImageVersion",
174
+ "ACCEPT_EULA",
175
+ "DEBIAN_FRONTEND",
176
+ "AGENT_TOOLSDIRECTORY",
177
+ "CONDA",
178
+ "SWIFT_PATH",
179
+ "LEIN_HOME",
180
+ "LEIN_JAR",
181
+ "ANT_HOME",
182
+ "GRADLE_HOME",
183
+ "M2_HOME",
184
+ "SELENIUM_JAR_PATH",
185
+ "ENABLE_RUNNER_TRACING",
186
+ "INVOCATION_ID",
187
+ "JOURNAL_STREAM",
188
+ "SYSTEMD_EXEC_PID",
189
+ "MANAGERPID",
190
+ "OLDPWD",
191
+ "PWD",
192
+ "SHLVL",
193
+ "_",
194
+ // GitHub Actions context (never GITHUB_TOKEN)
195
+ "GITHUB_ACTION",
196
+ "GITHUB_ACTION_PATH",
197
+ "GITHUB_ACTION_REF",
198
+ "GITHUB_ACTION_REPOSITORY",
199
+ "GITHUB_ACTOR",
200
+ "GITHUB_ACTOR_ID",
201
+ "GITHUB_ENV",
202
+ "GITHUB_EVENT_PATH",
203
+ "GITHUB_GRAPHQL_URL",
204
+ "GITHUB_JOB",
205
+ "GITHUB_OUTPUT",
206
+ "GITHUB_PATH",
207
+ "GITHUB_REF_PROTECTED",
208
+ "GITHUB_REF_TYPE",
209
+ "GITHUB_REPOSITORY_ID",
210
+ "GITHUB_REPOSITORY_OWNER_ID",
211
+ "GITHUB_RETENTION_DAYS",
212
+ "GITHUB_RUN_ATTEMPT",
213
+ "GITHUB_STATE",
214
+ "GITHUB_STEP_SUMMARY",
215
+ "GITHUB_TRIGGERING_ACTOR",
216
+ "GITHUB_WORKFLOW",
217
+ "GITHUB_WORKFLOW_REF",
218
+ "GITHUB_WORKFLOW_SHA",
219
+ ]);
220
+ /**
221
+ * Runner baseline: name patterns for families the CI images set in bulk
222
+ * (Actions runner internals, Android/Java/.NET/Go/Python toolchains, browsers
223
+ * and webdrivers, Azure CLI, vcpkg, ghcup, ...). `AWS_*`, `NPM_*` and
224
+ * `GITHUB_TOKEN`-style names are intentionally not matched.
225
+ */
226
+ export const RUNNER_BASELINE_PATTERNS = [
227
+ /^ACTIONS_/,
228
+ /^RUNNER_/,
229
+ /^ANDROID_/,
230
+ /^JAVA_HOME(_|$)/,
231
+ /^CHROME/,
232
+ /WEBDRIVER$/,
233
+ /^DOTNET_/,
234
+ /^GOROOT(_|$)/,
235
+ /^PIPX_/,
236
+ /^POWERSHELL_/,
237
+ /^VCPKG_/,
238
+ /^GHCUP_/,
239
+ /^BOOTSTRAP_HASKELL_/,
240
+ /^AZURE_(EXTENSION_DIR|HTTP_USER_AGENT|CONFIG_DIR)$/,
241
+ /^HOMEBREW_/,
242
+ /^STATS_/,
243
+ ];
244
+ /** True when `name` is set by the CI image itself, not by the workflow author. */
245
+ export function isRunnerBaselineVar(name) {
246
+ return RUNNER_BASELINE_VARS.has(name) || RUNNER_BASELINE_PATTERNS.some((re) => re.test(name));
247
+ }
248
+ /** Split withheld names into the runner baseline and everything else. */
249
+ export function classifyWithheld(withheld) {
250
+ const baseline = [];
251
+ const other = [];
252
+ for (const n of withheld)
253
+ (isRunnerBaselineVar(n) ? baseline : other).push(n);
254
+ return { baseline: baseline.sort(), other: other.sort() };
255
+ }
162
256
  /**
163
257
  * One-line warning naming the withheld variables. Names only, never values;
164
258
  * sorted; capped at {@link WITHHELD_WARNING_CAP}.
@@ -171,6 +265,37 @@ export function formatWithheldWarning(withheld) {
171
265
  `If a node's commands need any of them, add the names to env-passthrough ` +
172
266
  `(SWENY_ENV_PASSTHROUGH), or set env-scope: off (SWENY_ENV_SCOPE=off).`);
173
267
  }
268
+ /** Plain-log summary: counts only, no names. */
269
+ export function formatScopeSummary(total, baseline) {
270
+ return (`sweny: agent env scoped (${total} withheld, ${baseline} from the CI image). ` +
271
+ `Add names to env-passthrough if a node needs them; --verbose lists them.`);
272
+ }
273
+ let withheldReported = false;
274
+ /** Test seam: forget that the withheld notice was already emitted. */
275
+ export function resetWithheldReport() {
276
+ withheldReported = false;
277
+ }
278
+ /**
279
+ * Report withheld env names, once per process (not per node or per client).
280
+ *
281
+ * - always: one plain `info` line with counts (not a `::warning::` annotation);
282
+ * - only when non-baseline names were withheld: a `warn` listing just those
283
+ * (capped), as a `::warning title=SWEny agent env::` annotation under GitHub
284
+ * Actions;
285
+ * - `debug` (shown with `--verbose`): every withheld name.
286
+ */
287
+ export function reportWithheldEnv(withheld, logger, env = process.env) {
288
+ if (withheld.length === 0 || withheldReported)
289
+ return;
290
+ withheldReported = true;
291
+ const { baseline, other } = classifyWithheld(withheld);
292
+ logger.info(formatScopeSummary(withheld.length, baseline.length));
293
+ if (other.length > 0) {
294
+ const prefix = env.GITHUB_ACTIONS === "true" ? "::warning title=SWEny agent env::" : "";
295
+ logger.warn(`${prefix}${formatWithheldWarning(other)}`);
296
+ }
297
+ logger.debug(`sweny: agent env withheld: ${[...withheld].sort().join(", ")}`);
298
+ }
174
299
  /**
175
300
  * Scope the agent env and report which variable names were withheld.
176
301
  * `withheld` holds names only; values never leave `source`.
package/dist/claude.d.ts CHANGED
@@ -78,7 +78,9 @@ export interface ClaudeClientOptions {
78
78
  logger?: Logger;
79
79
  /** Default tool context for standalone usage (not via executor) */
80
80
  defaultContext?: ToolContext;
81
- /** External MCP servers (GitHub, Linear, Sentry, etc.) — merged with core skill tools */
81
+ /** Catalog defaults, overridden by per-run skill servers and explicit mcpServers. */
82
+ defaultMcpServers?: Record<string, McpServerConfig>;
83
+ /** Explicit external MCP servers, overriding defaults and per-run skill servers. */
82
84
  mcpServers?: Record<string, McpServerConfig>;
83
85
  /**
84
86
  * Extra env var names passed to the agent subprocess on top of the
@@ -123,13 +125,13 @@ export declare class ClaudeClient implements Claude {
123
125
  private logger;
124
126
  private defaultContext;
125
127
  private mcpServers;
128
+ private defaultMcpServers;
126
129
  private envPassthrough;
127
130
  private sandboxMode;
128
131
  private sandboxAllowedDomains;
129
132
  private sandboxProbe;
130
133
  private sandboxWarned;
131
134
  private envScope;
132
- private envWarned;
133
135
  constructor(opts?: ClaudeClientOptions);
134
136
  /**
135
137
  * Build env for the Claude Code subprocess (#360). Applies auth precedence
@@ -137,8 +139,8 @@ export declare class ClaudeClient implements Claude {
137
139
  * `SWENY_AUTH`). When env scoping is on (default in CI, see
138
140
  * {@link resolveEnvScope}) the result is narrowed to the allowlist plus
139
141
  * `extraVars` (the node's declared skill env vars) and the operator
140
- * passthrough list, and the withheld names (never values) are warned once
141
- * per client. When off, the full env passes through as before.
142
+ * passthrough list, and the withheld names (never values) are reported once
143
+ * per process ({@link reportWithheldEnv}). When off, the full env passes through as before.
142
144
  */
143
145
  private buildEnv;
144
146
  run(opts: {
@@ -154,6 +156,8 @@ export declare class ClaudeClient implements Claude {
154
156
  timeoutMs?: number;
155
157
  /** Caller-supplied abort signal. Aborting it interrupts the query. */
156
158
  signal?: AbortSignal;
159
+ /** MCP servers declared by the current node's skills. */
160
+ mcpServers?: Record<string, McpServerConfig>;
157
161
  /** Dry run (#380): no external MCP servers, no write-capable built-ins. */
158
162
  readOnly?: boolean;
159
163
  /** Env var names + sandbox hosts this node's skills need (#360). */
package/dist/claude.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude-agent-sdk";
12
12
  import { z } from "zod";
13
13
  import { consoleLogger } from "./types.js";
14
- import { formatWithheldWarning, parseList, resolveEnvScope, scopeAgentEnv, resolveAgentSandbox, } from "./agent-env.js";
14
+ import { reportWithheldEnv, parseList, resolveEnvScope, scopeAgentEnv, resolveAgentSandbox, } from "./agent-env.js";
15
15
  import { fenceUntrustedJson } from "./untrusted.js";
16
16
  const SYSTEM_PROMPT = `You are a step in an automated workflow. Execute the instruction precisely using the tools available to you. Be thorough but concise. When you're done, summarize your findings and results.`;
17
17
  /**
@@ -211,13 +211,13 @@ export class ClaudeClient {
211
211
  logger;
212
212
  defaultContext;
213
213
  mcpServers;
214
+ defaultMcpServers;
214
215
  envPassthrough;
215
216
  sandboxMode;
216
217
  sandboxAllowedDomains;
217
218
  sandboxProbe;
218
219
  sandboxWarned = false;
219
220
  envScope;
220
- envWarned = false;
221
221
  constructor(opts = {}) {
222
222
  this.model = opts.model;
223
223
  this.maxTurns = opts.maxTurns ?? 20;
@@ -225,6 +225,7 @@ export class ClaudeClient {
225
225
  this.logger = opts.logger ?? consoleLogger;
226
226
  this.defaultContext = opts.defaultContext ?? { config: {}, logger: this.logger };
227
227
  this.mcpServers = opts.mcpServers ?? {};
228
+ this.defaultMcpServers = opts.defaultMcpServers ?? {};
228
229
  this.envPassthrough = opts.envPassthrough;
229
230
  this.envScope = opts.envScope;
230
231
  this.sandboxMode = opts.sandbox;
@@ -237,8 +238,8 @@ export class ClaudeClient {
237
238
  * `SWENY_AUTH`). When env scoping is on (default in CI, see
238
239
  * {@link resolveEnvScope}) the result is narrowed to the allowlist plus
239
240
  * `extraVars` (the node's declared skill env vars) and the operator
240
- * passthrough list, and the withheld names (never values) are warned once
241
- * per client. When off, the full env passes through as before.
241
+ * passthrough list, and the withheld names (never values) are reported once
242
+ * per process ({@link reportWithheldEnv}). When off, the full env passes through as before.
242
243
  */
243
244
  buildEnv(extraVars = []) {
244
245
  const full = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
@@ -250,11 +251,7 @@ export class ClaudeClient {
250
251
  passthrough: this.envPassthrough ?? parseList(process.env.SWENY_ENV_PASSTHROUGH),
251
252
  logger: this.logger,
252
253
  });
253
- if (withheld.length > 0 && !this.envWarned) {
254
- this.envWarned = true;
255
- const prefix = process.env.GITHUB_ACTIONS === "true" ? "::warning title=SWEny agent env::" : "";
256
- this.logger.warn(`${prefix}${formatWithheldWarning(withheld)}`);
257
- }
254
+ reportWithheldEnv(withheld, this.logger);
258
255
  return env;
259
256
  }
260
257
  async run(opts) {
@@ -344,7 +341,9 @@ export class ClaudeClient {
344
341
  const abort = makeAbort(timeoutMs, signal);
345
342
  let stream;
346
343
  try {
347
- const allMcpServers = readOnly ? {} : { ...this.mcpServers };
344
+ const allMcpServers = readOnly
345
+ ? {}
346
+ : { ...this.defaultMcpServers, ...opts.mcpServers, ...this.mcpServers };
348
347
  if (sdkTools.length > 0)
349
348
  allMcpServers["sweny-core"] = mcpServer;
350
349
  stream = query({
package/dist/cli/main.js CHANGED
@@ -204,7 +204,8 @@ triageCmd.action(async (options) => {
204
204
  maxTurns: config.maxInvestigateTurns || 50,
205
205
  cwd: process.cwd(),
206
206
  logger: consoleLogger,
207
- mcpServers,
207
+ defaultMcpServers: mcpServers,
208
+ mcpServers: config.mcpServers,
208
209
  });
209
210
  // ── Progress display state ─────────────────────────────────
210
211
  const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -513,7 +514,8 @@ implementCmd.action(async (issueId, options) => {
513
514
  maxTurns: config.maxImplementTurns || 40,
514
515
  cwd: process.cwd(),
515
516
  logger: consoleLogger,
516
- mcpServers,
517
+ defaultMcpServers: mcpServers,
518
+ mcpServers: config.mcpServers,
517
519
  });
518
520
  console.log(chalk.cyan(`\n sweny implement ${issueId}\n`));
519
521
  const implRunStart = Date.now();
@@ -756,7 +758,8 @@ export async function workflowRunAction(file, options) {
756
758
  maxTurns: config.maxInvestigateTurns || 50,
757
759
  cwd: process.cwd(),
758
760
  logger: runLogger,
759
- mcpServers,
761
+ defaultMcpServers: mcpServers,
762
+ mcpServers: config.mcpServers,
760
763
  model: workflow.model,
761
764
  });
762
765
  // Track per-node entry time to compute elapsed on exit
package/dist/executor.js CHANGED
@@ -148,12 +148,16 @@ export async function execute(workflow, input, options) {
148
148
  logger.info(` dry run: withheld write tools: ${skippedWrites.join(", ")}`, { node: currentId });
149
149
  }
150
150
  const skillInstructions = resolveSkillInstructions(node.skills, skills);
151
+ const skillMcpServers = resolveSkillMcpServers(node.skills, skills);
151
152
  // Runtime guard: if this node declares skills but none resolved, the node
152
153
  // cannot do its job (e.g. "create a Linear issue" with no linear skill).
153
154
  // The startup validate() warns about this possibility, but only throw when
154
155
  // the node is actually reached — unreachable nodes with missing skills are fine.
155
- // Instruction-only skills (no tools) are valid — they inject context into the prompt.
156
- if (node.skills.length > 0 && resolvedSkillTools.length === 0 && skillInstructions.length === 0) {
156
+ // Instruction-only and resolved MCP-only skills are valid capabilities.
157
+ if (node.skills.length > 0 &&
158
+ resolvedSkillTools.length === 0 &&
159
+ skillInstructions.length === 0 &&
160
+ Object.keys(skillMcpServers).length === 0) {
157
161
  throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
158
162
  `Set the required environment variables and try again.`);
159
163
  }
@@ -241,6 +245,7 @@ export async function execute(workflow, input, options) {
241
245
  signal,
242
246
  timeoutMs,
243
247
  agentAccess: resolveAgentAccess(node.skills, skills),
248
+ ...(dryRun ? {} : { mcpServers: skillMcpServers }),
244
249
  ...(dryRun ? { readOnly: true } : {}),
245
250
  onProgress: (message) => {
246
251
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
@@ -721,6 +726,13 @@ function dryRunNotice(skippedWrites) {
721
726
  `Do not create, modify, post, or send anything. Do the analysis, and where this step would ` +
722
727
  `normally write, describe exactly what it would have written instead.`);
723
728
  }
729
+ /** Only this node's resolved skills contribute external servers. */
730
+ function resolveSkillMcpServers(skillIds, skills) {
731
+ return Object.fromEntries(skillIds.flatMap((id) => {
732
+ const mcp = skills.get(id)?.mcp;
733
+ return mcp ? [[id, { ...mcp, type: mcp.type ?? (mcp.command ? "stdio" : "http") }]] : [];
734
+ }));
735
+ }
724
736
  function resolveTools(skillIds, skills) {
725
737
  return skillIds
726
738
  .map((id) => skills.get(id))
@@ -1023,6 +1035,14 @@ async function resolveNext(workflow, current, results, input, claude, observer,
1023
1035
  * Validate a workflow definition before execution.
1024
1036
  */
1025
1037
  function validate(workflow, skills) {
1038
+ for (const [id, def] of Object.entries(workflow.skills ?? {})) {
1039
+ if (!def.instruction?.trim()) {
1040
+ throw new Error(`Inline skill "${id}" must provide a non-empty instruction`);
1041
+ }
1042
+ if (id === "sweny-core" && def.mcp) {
1043
+ throw new Error(`Skill "sweny-core" is reserved for the engine MCP server; use a different skill ID`);
1044
+ }
1045
+ }
1026
1046
  if (!workflow.nodes[workflow.entry]) {
1027
1047
  throw new Error(`Entry node "${workflow.entry}" not found`);
1028
1048
  }
@@ -1036,6 +1056,12 @@ function validate(workflow, skills) {
1036
1056
  for (const [nodeId, node] of Object.entries(workflow.nodes)) {
1037
1057
  if (node.skills.length === 0)
1038
1058
  continue;
1059
+ for (const id of node.skills) {
1060
+ const skill = skills.get(id);
1061
+ if (id === "sweny-core" && skill?.mcp) {
1062
+ throw new Error(`Skill "sweny-core" is reserved for the engine MCP server; use a different skill ID (node "${nodeId}")`);
1063
+ }
1064
+ }
1039
1065
  const available = node.skills.filter((id) => skills.has(id));
1040
1066
  if (available.length === 0) {
1041
1067
  consoleLogger.warn(`Node "${nodeId}" has no available skills (needs one of: ${node.skills.join(", ")})`);
package/dist/schema.d.ts CHANGED
@@ -3357,11 +3357,7 @@ export declare const workflowJsonSchema: {
3357
3357
  readonly description: "Inline skill definitions scoped to this workflow";
3358
3358
  readonly additionalProperties: {
3359
3359
  readonly type: "object";
3360
- readonly anyOf: readonly [{
3361
- readonly required: readonly ["instruction"];
3362
- }, {
3363
- readonly required: readonly ["mcp"];
3364
- }];
3360
+ readonly required: readonly ["instruction"];
3365
3361
  readonly additionalProperties: false;
3366
3362
  readonly properties: {
3367
3363
  readonly name: {
@@ -3372,6 +3368,7 @@ export declare const workflowJsonSchema: {
3372
3368
  };
3373
3369
  readonly instruction: {
3374
3370
  readonly type: "string";
3371
+ readonly pattern: "\\S";
3375
3372
  readonly description: "Natural language expertise injected into the node prompt";
3376
3373
  };
3377
3374
  readonly mcp: {
package/dist/schema.js CHANGED
@@ -63,8 +63,8 @@ export const skillDefinitionZ = z
63
63
  mcp: mcpServerConfigZ.optional(),
64
64
  })
65
65
  .strict()
66
- .refine((s) => s.instruction || s.mcp, {
67
- message: "Inline skill must provide instruction, mcp, or both",
66
+ .refine((s) => Boolean(s.instruction?.trim()), {
67
+ message: "Inline skill must provide a non-empty instruction",
68
68
  });
69
69
  export const skillZ = z
70
70
  .object({
@@ -489,12 +489,18 @@ export function validateWorkflow(workflow, knownSkills) {
489
489
  });
490
490
  }
491
491
  }
492
- // Inline skill definitions must have instruction or mcp
492
+ // Inline skills need usage instructions; the engine server name is reserved.
493
493
  for (const [skillId, def] of Object.entries(workflow.skills ?? {})) {
494
- if (!def.instruction && !def.mcp) {
494
+ if (!def.instruction?.trim()) {
495
495
  errors.push({
496
496
  code: "INVALID_INLINE_SKILL",
497
- message: `Inline skill "${skillId}" must provide at least instruction or mcp`,
497
+ message: `Inline skill "${skillId}" must provide a non-empty instruction`,
498
+ });
499
+ }
500
+ if (skillId === "sweny-core" && def.mcp) {
501
+ errors.push({
502
+ code: "INVALID_INLINE_SKILL",
503
+ message: `Skill "sweny-core" is reserved for the engine MCP server; use a different skill ID`,
498
504
  });
499
505
  }
500
506
  }
@@ -945,14 +951,18 @@ export const workflowJsonSchema = {
945
951
  description: "Inline skill definitions scoped to this workflow",
946
952
  additionalProperties: {
947
953
  type: "object",
948
- // Fix #4: an inline skill must provide instruction, mcp, or both.
949
- anyOf: [{ required: ["instruction"] }, { required: ["mcp"] }],
954
+ // Inline skills need usable instructions, including when they declare MCP.
955
+ required: ["instruction"],
950
956
  // Round 2: reject unknown keys to match Zod skillDefinitionZ.strict().
951
957
  additionalProperties: false,
952
958
  properties: {
953
959
  name: { type: "string" },
954
960
  description: { type: "string" },
955
- instruction: { type: "string", description: "Natural language expertise injected into the node prompt" },
961
+ instruction: {
962
+ type: "string",
963
+ pattern: "\\S",
964
+ description: "Natural language expertise injected into the node prompt",
965
+ },
956
966
  mcp: {
957
967
  type: "object",
958
968
  description: "External MCP server definition",
package/dist/types.d.ts CHANGED
@@ -604,6 +604,8 @@ export interface Claude {
604
604
  outputSchema?: JSONSchema;
605
605
  /** Called with status messages while Claude is working (tool name, etc.) */
606
606
  onProgress?: (message: string) => void;
607
+ /** MCP servers declared by this node's resolved skills. Explicit client configs win. */
608
+ mcpServers?: Record<string, McpServerConfig>;
607
609
  /** Per-node turn limit. Overrides the client default when set. */
608
610
  maxTurns?: number;
609
611
  /** Built-in SDK tool names to disallow for this node (e.g. ["Bash"]). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.125",
3
+ "version": "0.1.127",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"