@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.
- package/dist/agent-env.d.ts +37 -0
- package/dist/agent-env.js +125 -0
- package/dist/claude.d.ts +8 -4
- package/dist/claude.js +9 -10
- package/dist/cli/main.js +6 -3
- package/dist/executor.js +28 -2
- package/dist/schema.d.ts +2 -5
- package/dist/schema.js +18 -8
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/agent-env.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
141
|
-
* per
|
|
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 {
|
|
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
|
|
241
|
-
* per
|
|
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
|
-
|
|
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
|
|
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
|
|
156
|
-
if (node.skills.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
|
|
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
|
|
67
|
-
message: "Inline skill must provide
|
|
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
|
|
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
|
|
494
|
+
if (!def.instruction?.trim()) {
|
|
495
495
|
errors.push({
|
|
496
496
|
code: "INVALID_INLINE_SKILL",
|
|
497
|
-
message: `Inline skill "${skillId}" must provide
|
|
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
|
-
//
|
|
949
|
-
|
|
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: {
|
|
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"]). */
|