@sweny-ai/core 0.1.102 → 0.1.104
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/browser.d.ts +3 -2
- package/dist/browser.js +6 -1
- package/dist/claude.d.ts +66 -3
- package/dist/claude.js +334 -29
- package/dist/cli/check.d.ts +7 -0
- package/dist/cli/check.js +91 -5
- package/dist/cli/config-file.js +5 -1
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +19 -1
- package/dist/cli/e2e.d.ts +14 -1
- package/dist/cli/e2e.js +40 -6
- package/dist/cli/main.d.ts +3 -1
- package/dist/cli/main.js +26 -16
- package/dist/cli/new.js +7 -0
- package/dist/eval/value.js +12 -1
- package/dist/executor.d.ts +16 -0
- package/dist/executor.js +81 -18
- package/dist/index.d.ts +8 -4
- package/dist/index.js +10 -2
- package/dist/inputs.js +18 -0
- package/dist/loader.js +15 -1
- package/dist/model.d.ts +11 -0
- package/dist/model.js +19 -0
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +2 -2
- package/dist/schema.d.ts +163 -6
- package/dist/schema.js +125 -10
- package/dist/skills/custom-loader.d.ts +3 -3
- package/dist/skills/custom-loader.js +62 -20
- package/dist/skills/github.js +8 -2
- package/dist/skills/linear.js +26 -0
- package/dist/skills/notification.d.ts +17 -0
- package/dist/skills/notification.js +67 -5
- package/dist/source-resolver.js +231 -6
- package/dist/sources.d.ts +33 -0
- package/dist/types.d.ts +29 -1
- package/dist/types.js +17 -0
- package/package.json +4 -4
package/dist/cli/check.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-check network timeout. A hung connect must not make `sweny check` hang
|
|
3
|
+
* indefinitely — the whole point of the command is fast diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
const CHECK_TIMEOUT_MS = 5000;
|
|
1
6
|
/**
|
|
2
7
|
* Run lightweight connectivity checks for all configured providers.
|
|
3
8
|
* Uses raw fetch — does NOT import provider packages.
|
|
@@ -7,17 +12,24 @@ export async function checkProviderConnectivity(config) {
|
|
|
7
12
|
const githubChecked = { done: false }; // avoid double-checking same token
|
|
8
13
|
// ── Coding agent ────────────────────────────────────────────────────────────
|
|
9
14
|
if (config.codingAgentProvider === "claude") {
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const mode = resolveCheckAuthMode(config);
|
|
16
|
+
const base = config.anthropicBaseUrl;
|
|
17
|
+
if (mode === "none") {
|
|
18
|
+
results.push({ name: "Anthropic (claude agent)", status: "skip", detail: "No credential configured" });
|
|
19
|
+
}
|
|
20
|
+
else if (base) {
|
|
21
|
+
// Gateway: probe the gateway, never real Anthropic with a gateway-bound key.
|
|
22
|
+
results.push(await checkAnthropicGateway(base, config, mode));
|
|
12
23
|
}
|
|
13
|
-
else if (
|
|
24
|
+
else if (mode === "api-key") {
|
|
14
25
|
results.push(await checkAnthropic(config.anthropicApiKey));
|
|
15
26
|
}
|
|
16
27
|
else {
|
|
28
|
+
// oauth or auth-token, no base URL: nothing cheap to probe, report the mode.
|
|
17
29
|
results.push({
|
|
18
30
|
name: "Anthropic (claude agent)",
|
|
19
31
|
status: "skip",
|
|
20
|
-
detail:
|
|
32
|
+
detail: `auth mode: ${mode}, skipping upstream API check`,
|
|
21
33
|
});
|
|
22
34
|
}
|
|
23
35
|
}
|
|
@@ -105,6 +117,7 @@ async function checkAnthropic(apiKey) {
|
|
|
105
117
|
try {
|
|
106
118
|
const res = await fetch("https://api.anthropic.com/v1/models", {
|
|
107
119
|
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
120
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
108
121
|
});
|
|
109
122
|
if (res.status === 200) {
|
|
110
123
|
return { name, status: "ok", detail: "API key is valid" };
|
|
@@ -122,15 +135,79 @@ async function checkAnthropic(apiKey) {
|
|
|
122
135
|
return { name, status: "fail", detail: networkErrorMessage(err) };
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
|
-
|
|
138
|
+
export function resolveCheckAuthMode(config) {
|
|
139
|
+
const hasKey = !!config.anthropicApiKey;
|
|
140
|
+
const hasBearer = !!config.anthropicAuthToken;
|
|
141
|
+
const hasOauth = !!config.claudeOauthToken;
|
|
142
|
+
if (config.swenyAuth === "oauth")
|
|
143
|
+
return hasOauth ? "oauth" : "none";
|
|
144
|
+
if (config.swenyAuth === "api-key")
|
|
145
|
+
return hasKey ? "api-key" : hasBearer ? "auth-token" : "none";
|
|
146
|
+
// auto: OAuth wins when present (today's protective behavior)
|
|
147
|
+
if (hasOauth)
|
|
148
|
+
return "oauth";
|
|
149
|
+
if (hasKey)
|
|
150
|
+
return "api-key";
|
|
151
|
+
if (hasBearer)
|
|
152
|
+
return "auth-token";
|
|
153
|
+
return "none";
|
|
154
|
+
}
|
|
155
|
+
/** Redact a URL to scheme + host so userinfo / query (which can carry a key) never logs. */
|
|
156
|
+
export function redactUrl(raw) {
|
|
157
|
+
try {
|
|
158
|
+
const u = new URL(raw);
|
|
159
|
+
return `${u.protocol}//${u.host}`;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return "(invalid URL)";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function checkAnthropicGateway(base, config, mode) {
|
|
166
|
+
const name = "Anthropic (gateway)";
|
|
167
|
+
const safeBase = redactUrl(base);
|
|
168
|
+
const headers = { "anthropic-version": "2023-06-01" };
|
|
169
|
+
if (mode === "api-key") {
|
|
170
|
+
headers["x-api-key"] = config.anthropicApiKey;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// auth-token or oauth against the gateway
|
|
174
|
+
headers.Authorization = `Bearer ${config.anthropicAuthToken || config.claudeOauthToken}`;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const url = base.replace(/\/+$/, "") + "/v1/models";
|
|
178
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(CHECK_TIMEOUT_MS) });
|
|
179
|
+
// Gateways vary on whether /v1/models exists; reachable + non-auth-error is good enough.
|
|
180
|
+
if (res.ok || res.status === 404) {
|
|
181
|
+
return { name, status: "ok", detail: `gateway reachable (${safeBase}), auth mode: ${mode}` };
|
|
182
|
+
}
|
|
183
|
+
if (res.status === 401 || res.status === 403) {
|
|
184
|
+
return { name, status: "fail", detail: `${res.status} from gateway ${safeBase}, check gateway credentials` };
|
|
185
|
+
}
|
|
186
|
+
return { name, status: "fail", detail: `Unexpected HTTP ${res.status} from gateway ${safeBase}` };
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
return { name, status: "fail", detail: networkErrorMessage(err) };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function checkDatadog(creds) {
|
|
126
193
|
const name = "Observability (datadog)";
|
|
127
194
|
const { apiKey, appKey, site = "datadoghq.com" } = creds;
|
|
128
195
|
if (!apiKey || !appKey) {
|
|
129
196
|
return { name, status: "skip", detail: "DD_API_KEY or DD_APP_KEY not configured" };
|
|
130
197
|
}
|
|
198
|
+
// `site` (DD_SITE) is interpolated into the request URL — reject anything
|
|
199
|
+
// outside a hostname allowlist before it can smuggle a path/query/host.
|
|
200
|
+
if (!/^[a-z0-9.-]+$/.test(site)) {
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
status: "fail",
|
|
204
|
+
detail: `Invalid DD_SITE "${site}" — expected a hostname like datadoghq.com or datadoghq.eu`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
131
207
|
try {
|
|
132
208
|
const res = await fetch(`https://api.${site}/api/v2/validate`, {
|
|
133
209
|
headers: { "DD-API-KEY": apiKey, "DD-APPLICATION-KEY": appKey },
|
|
210
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
134
211
|
});
|
|
135
212
|
if (res.status === 200) {
|
|
136
213
|
return { name, status: "ok", detail: "API key is valid" };
|
|
@@ -157,6 +234,7 @@ async function checkSentry(creds) {
|
|
|
157
234
|
try {
|
|
158
235
|
const res = await fetch("https://sentry.io/api/0/", {
|
|
159
236
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
237
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
160
238
|
});
|
|
161
239
|
if (res.status === 200) {
|
|
162
240
|
return { name, status: "ok", detail: "Auth token is valid" };
|
|
@@ -184,6 +262,7 @@ async function checkLinear(apiKey) {
|
|
|
184
262
|
method: "POST",
|
|
185
263
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
186
264
|
body: JSON.stringify({ query: "{ viewer { id name } }" }),
|
|
265
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
187
266
|
});
|
|
188
267
|
if (res.status === 200) {
|
|
189
268
|
const body = (await res.json());
|
|
@@ -213,6 +292,7 @@ async function checkGitHub(token, name) {
|
|
|
213
292
|
try {
|
|
214
293
|
const res = await fetch("https://api.github.com/user", {
|
|
215
294
|
headers: { Authorization: `token ${token}`, "User-Agent": "sweny-cli" },
|
|
295
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
216
296
|
});
|
|
217
297
|
if (res.status === 200) {
|
|
218
298
|
const body = (await res.json());
|
|
@@ -232,6 +312,12 @@ async function checkGitHub(token, name) {
|
|
|
232
312
|
}
|
|
233
313
|
}
|
|
234
314
|
function networkErrorMessage(err) {
|
|
315
|
+
// AbortSignal.timeout fires a DOMException with name "TimeoutError"; an
|
|
316
|
+
// explicit abort surfaces as "AbortError". Either means we gave up waiting.
|
|
317
|
+
const errName = err instanceof Error ? err.name : "";
|
|
318
|
+
if (errName === "TimeoutError" || errName === "AbortError") {
|
|
319
|
+
return `Connection timed out after ${CHECK_TIMEOUT_MS}ms — host unreachable or too slow`;
|
|
320
|
+
}
|
|
235
321
|
const msg = err instanceof Error ? err.message : String(err);
|
|
236
322
|
if (/ENOTFOUND|ETIMEDOUT|ECONNREFUSED/i.test(msg)) {
|
|
237
323
|
return `Network error — check your internet connection (${msg})`;
|
package/dist/cli/config-file.js
CHANGED
|
@@ -52,7 +52,11 @@ export function loadConfigFile(cwd = process.cwd()) {
|
|
|
52
52
|
try {
|
|
53
53
|
raw = parseYaml(content);
|
|
54
54
|
}
|
|
55
|
-
catch {
|
|
55
|
+
catch (err) {
|
|
56
|
+
// A malformed config file must not silently fall back to defaults — the
|
|
57
|
+
// user would get unexpected behavior with no clue their file was dropped.
|
|
58
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.warn(`⚠ Ignoring malformed config file ${filePath}: ${reason}`);
|
|
56
60
|
return {};
|
|
57
61
|
}
|
|
58
62
|
if (!raw || typeof raw !== "object")
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -4,7 +4,12 @@ import type { FileConfig } from "./config-file.js";
|
|
|
4
4
|
export interface CliConfig {
|
|
5
5
|
codingAgentProvider: string;
|
|
6
6
|
anthropicApiKey: string;
|
|
7
|
+
anthropicAuthToken: string;
|
|
7
8
|
claudeOauthToken: string;
|
|
9
|
+
/** Anthropic-compatible base URL (e.g. a LiteLLM gateway). */
|
|
10
|
+
anthropicBaseUrl: string;
|
|
11
|
+
/** Auth precedence mode: auto (default), api-key, or oauth. */
|
|
12
|
+
swenyAuth: "auto" | "api-key" | "oauth";
|
|
8
13
|
openaiApiKey: string;
|
|
9
14
|
geminiApiKey: string;
|
|
10
15
|
observabilityProviders: string[];
|
|
@@ -82,6 +87,12 @@ export declare function parseCliInputs(options: Record<string, unknown>, fileCon
|
|
|
82
87
|
*/
|
|
83
88
|
export declare const SUPPORTED_WORKSPACE_TOOLS: Set<string>;
|
|
84
89
|
export declare function validateInputs(config: CliConfig): string[];
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a SWENY_AUTH value. Unknown / empty values fall back to "auto"
|
|
92
|
+
* (the safe, billing-protective default). Never throws: an env typo must not
|
|
93
|
+
* crash a run, and `auto` only ever strips a key to protect a subscription.
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeSwenyAuth(raw: unknown): "auto" | "api-key" | "oauth";
|
|
85
96
|
/**
|
|
86
97
|
* Parse a value as a positive integer (>= 1).
|
|
87
98
|
*
|
package/dist/cli/config.js
CHANGED
|
@@ -59,7 +59,7 @@ export function registerTriageCommand(program) {
|
|
|
59
59
|
.option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.", false)
|
|
60
60
|
.option("--bell", "Ring terminal bell on completion", false)
|
|
61
61
|
.option("--cache-dir <path>", "Step cache directory (default: .sweny/cache)")
|
|
62
|
-
.option("--cache-ttl <seconds>", "Cache TTL in seconds,
|
|
62
|
+
.option("--cache-ttl <seconds>", "Cache TTL in seconds, 1 to 31536000 (default: 86400)")
|
|
63
63
|
.option("--no-cache", "Disable step cache")
|
|
64
64
|
.option("--output-dir <path>", "Output directory for file providers (default: .sweny/output)")
|
|
65
65
|
.option("--workspace-tools <tools>", "Comma-separated workspace tool integrations to enable (slack, notion, pagerduty, monday)")
|
|
@@ -100,7 +100,11 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
100
100
|
codingAgentProvider: options.agent || options.codingAgentProvider || f("coding-agent-provider") || "claude",
|
|
101
101
|
// Secrets: env only — never from config file
|
|
102
102
|
anthropicApiKey: env.ANTHROPIC_API_KEY || "",
|
|
103
|
+
anthropicAuthToken: env.ANTHROPIC_AUTH_TOKEN || "",
|
|
103
104
|
claudeOauthToken: env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
|
105
|
+
// Non-secret config: env or .sweny config file (mirrors gitlabBaseUrl)
|
|
106
|
+
anthropicBaseUrl: env.ANTHROPIC_BASE_URL || f("anthropic-base-url") || "",
|
|
107
|
+
swenyAuth: normalizeSwenyAuth(env.SWENY_AUTH || f("sweny-auth")),
|
|
104
108
|
openaiApiKey: env.OPENAI_API_KEY || "",
|
|
105
109
|
geminiApiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
|
|
106
110
|
observabilityProviders: obsProviders,
|
|
@@ -431,6 +435,9 @@ export function validateInputs(config) {
|
|
|
431
435
|
// bounds check let junk through).
|
|
432
436
|
validateIntegerBound(errors, "--max-investigate-turns", config.maxInvestigateTurns, 1, 500);
|
|
433
437
|
validateIntegerBound(errors, "--max-implement-turns", config.maxImplementTurns, 1, 500);
|
|
438
|
+
// cacheTtl is parsed with parsePositiveInt (junk → NaN); bound it so a
|
|
439
|
+
// malformed --cache-ttl / cache-ttl: surfaces a field error instead of NaN.
|
|
440
|
+
validateIntegerBound(errors, "--cache-ttl", config.cacheTtl, 1, 31_536_000);
|
|
434
441
|
return errors;
|
|
435
442
|
}
|
|
436
443
|
function validateIntegerBound(errors, flag, value, min, max) {
|
|
@@ -442,6 +449,17 @@ function validateIntegerBound(errors, flag, value, min, max) {
|
|
|
442
449
|
errors.push(`${flag} must be between ${min} and ${max}`);
|
|
443
450
|
}
|
|
444
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Normalize a SWENY_AUTH value. Unknown / empty values fall back to "auto"
|
|
454
|
+
* (the safe, billing-protective default). Never throws: an env typo must not
|
|
455
|
+
* crash a run, and `auto` only ever strips a key to protect a subscription.
|
|
456
|
+
*/
|
|
457
|
+
export function normalizeSwenyAuth(raw) {
|
|
458
|
+
const v = String(raw ?? "")
|
|
459
|
+
.trim()
|
|
460
|
+
.toLowerCase();
|
|
461
|
+
return v === "api-key" || v === "oauth" ? v : "auto";
|
|
462
|
+
}
|
|
445
463
|
/**
|
|
446
464
|
* Parse a value as a positive integer (>= 1).
|
|
447
465
|
*
|
package/dist/cli/e2e.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sweny e2e —
|
|
2
|
+
* sweny e2e (now `sweny new e2e` + `sweny workflow run`) — generate and run agent-driven end-to-end browser tests.
|
|
3
3
|
*
|
|
4
4
|
* Pure functions for workflow generation + thin @clack/prompts interactive layer.
|
|
5
5
|
* Tests cover the pure functions; the interactive wizard is thin glue.
|
|
@@ -78,7 +78,20 @@ export declare function runE2eInit(options?: E2eInitOptions): Promise<void>;
|
|
|
78
78
|
export interface E2eRunOptions {
|
|
79
79
|
file?: string;
|
|
80
80
|
timeout?: number;
|
|
81
|
+
/** Skip the batch confirmation prompt (CI). Only applies to no-file batch runs. */
|
|
82
|
+
yes?: boolean;
|
|
81
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Policy for a no-file batch run. Pure so it can be unit-tested without
|
|
86
|
+
* touching the filesystem, stdin, or the engine.
|
|
87
|
+
* - `yes` → run without asking (CI).
|
|
88
|
+
* - non-interactive without `yes` → refuse (don't fire every workflow by accident).
|
|
89
|
+
* - interactive → ask for confirmation.
|
|
90
|
+
*/
|
|
91
|
+
export declare function batchRunDecision(opts: {
|
|
92
|
+
yes?: boolean;
|
|
93
|
+
isTTY: boolean;
|
|
94
|
+
}): "run" | "confirm" | "refuse-noninteractive";
|
|
82
95
|
/**
|
|
83
96
|
* Load and execute e2e workflow files from .sweny/e2e/.
|
|
84
97
|
*/
|
package/dist/cli/e2e.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sweny e2e —
|
|
2
|
+
* sweny e2e (now `sweny new e2e` + `sweny workflow run`) — generate and run agent-driven end-to-end browser tests.
|
|
3
3
|
*
|
|
4
4
|
* Pure functions for workflow generation + thin @clack/prompts interactive layer.
|
|
5
5
|
* Tests cover the pure functions; the interactive wizard is thin glue.
|
|
@@ -501,7 +501,7 @@ const CLEANUP_ENV_VARS = {
|
|
|
501
501
|
export function buildE2eEnvTemplate(selections) {
|
|
502
502
|
const lines = [];
|
|
503
503
|
lines.push("# E2E Testing — SWEny");
|
|
504
|
-
lines.push("# Fill in values, then run: sweny
|
|
504
|
+
lines.push("# Fill in values, then run: sweny workflow run");
|
|
505
505
|
lines.push("");
|
|
506
506
|
lines.push(`E2E_BASE_URL=${selections.baseUrl}`);
|
|
507
507
|
lines.push("");
|
|
@@ -753,10 +753,10 @@ export async function runE2eInit(options = {}) {
|
|
|
753
753
|
console.log(chalk.dim(" 1. Fill in your .env values"));
|
|
754
754
|
if (selections.flows.some((f) => AUTH_REQUIRED_FLOWS.includes(f.type) || f.type === "login")) {
|
|
755
755
|
console.log(chalk.dim(" 2. Set E2E_EMAIL and E2E_PASSWORD to a test account"));
|
|
756
|
-
console.log(chalk.dim(` 3. Run: ${chalk.cyan("sweny
|
|
756
|
+
console.log(chalk.dim(` 3. Run: ${chalk.cyan("sweny workflow run")}\n`));
|
|
757
757
|
}
|
|
758
758
|
else {
|
|
759
|
-
console.log(chalk.dim(` 2. Run: ${chalk.cyan("sweny
|
|
759
|
+
console.log(chalk.dim(` 2. Run: ${chalk.cyan("sweny workflow run")}\n`));
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
762
|
// ── Timeout helper ─────────────────────────────────────────────────────
|
|
@@ -769,6 +769,20 @@ function withTimeout(promise, ms, label) {
|
|
|
769
769
|
.finally(() => clearTimeout(timer));
|
|
770
770
|
});
|
|
771
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Policy for a no-file batch run. Pure so it can be unit-tested without
|
|
774
|
+
* touching the filesystem, stdin, or the engine.
|
|
775
|
+
* - `yes` → run without asking (CI).
|
|
776
|
+
* - non-interactive without `yes` → refuse (don't fire every workflow by accident).
|
|
777
|
+
* - interactive → ask for confirmation.
|
|
778
|
+
*/
|
|
779
|
+
export function batchRunDecision(opts) {
|
|
780
|
+
if (opts.yes)
|
|
781
|
+
return "run";
|
|
782
|
+
if (!opts.isTTY)
|
|
783
|
+
return "refuse-noninteractive";
|
|
784
|
+
return "confirm";
|
|
785
|
+
}
|
|
772
786
|
/**
|
|
773
787
|
* Load and execute e2e workflow files from .sweny/e2e/.
|
|
774
788
|
*/
|
|
@@ -789,7 +803,7 @@ export async function runE2eRun(options) {
|
|
|
789
803
|
const e2eDir = path.join(cwd, ".sweny", "e2e");
|
|
790
804
|
if (!fs.existsSync(e2eDir)) {
|
|
791
805
|
console.error(chalk.red(" No .sweny/e2e/ directory found."));
|
|
792
|
-
console.error(chalk.dim(" Run 'sweny e2e
|
|
806
|
+
console.error(chalk.dim(" Run 'sweny new e2e' to set up e2e testing."));
|
|
793
807
|
process.exit(1);
|
|
794
808
|
}
|
|
795
809
|
files = fs
|
|
@@ -799,9 +813,29 @@ export async function runE2eRun(options) {
|
|
|
799
813
|
.map((f) => path.join(e2eDir, f));
|
|
800
814
|
if (files.length === 0) {
|
|
801
815
|
console.error(chalk.red(" No workflow files found in .sweny/e2e/"));
|
|
802
|
-
console.error(chalk.dim(" Run 'sweny e2e
|
|
816
|
+
console.error(chalk.dim(" Run 'sweny new e2e' to set up e2e testing."));
|
|
803
817
|
process.exit(1);
|
|
804
818
|
}
|
|
819
|
+
// Batch run (no explicit file): list what will run and confirm, so an
|
|
820
|
+
// accidental `sweny workflow run` doesn't fire every e2e workflow.
|
|
821
|
+
// --yes bypasses for CI.
|
|
822
|
+
const decision = batchRunDecision({ yes: options.yes, isTTY: process.stdin.isTTY ?? false });
|
|
823
|
+
if (decision !== "run") {
|
|
824
|
+
console.log(chalk.bold(`\n About to run ${files.length} e2e workflow(s) from .sweny/e2e/:`));
|
|
825
|
+
for (const f of files)
|
|
826
|
+
console.log(chalk.dim(` • ${path.basename(f)}`));
|
|
827
|
+
console.log("");
|
|
828
|
+
if (decision === "refuse-noninteractive") {
|
|
829
|
+
console.error(chalk.red(" Refusing to batch-run without confirmation in a non-interactive shell."));
|
|
830
|
+
console.error(chalk.dim(" Re-run with --yes to proceed (or pass a specific file)."));
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const ok = await p.confirm({ message: `Run all ${files.length} e2e workflow(s)?` });
|
|
834
|
+
if (p.isCancel(ok) || !ok) {
|
|
835
|
+
p.cancel("Cancelled.");
|
|
836
|
+
process.exit(0);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
805
839
|
}
|
|
806
840
|
// 2. Build template vars
|
|
807
841
|
const vars = buildE2eVars(process.env);
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import type { Workflow } from "../types.js";
|
|
3
3
|
export declare function loadWorkflowFile(filePath: string, knownSkills?: Set<string>): Workflow;
|
|
4
|
-
export declare function workflowRunAction(file: string, options: Record<string, unknown> & {
|
|
4
|
+
export declare function workflowRunAction(file: string | undefined, options: Record<string, unknown> & {
|
|
5
5
|
json?: boolean;
|
|
6
6
|
stream?: boolean;
|
|
7
7
|
mermaid?: boolean;
|
|
8
8
|
verbose?: boolean;
|
|
9
|
+
timeout?: string;
|
|
10
|
+
yes?: boolean;
|
|
9
11
|
}): Promise<void>;
|
|
10
12
|
export declare function workflowExportAction(name: string): void;
|
|
11
13
|
export declare function workflowValidateAction(file: string, options: {
|
package/dist/cli/main.js
CHANGED
|
@@ -27,7 +27,7 @@ import { buildCredentialMap } from "./credentials.js";
|
|
|
27
27
|
import { runNew } from "./new.js";
|
|
28
28
|
import { runE2eRun } from "./e2e.js";
|
|
29
29
|
import { createVerboseToolObserver } from "./verbose-observer.js";
|
|
30
|
-
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, } from "./config.js";
|
|
30
|
+
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, parsePositiveInt, } from "./config.js";
|
|
31
31
|
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, formatResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
32
32
|
import { checkProviderConnectivity } from "./check.js";
|
|
33
33
|
import { registerSetupCommand } from "./setup.js";
|
|
@@ -75,18 +75,6 @@ program
|
|
|
75
75
|
.action(async (id) => {
|
|
76
76
|
await runNew(id ? { marketplaceId: id } : undefined);
|
|
77
77
|
});
|
|
78
|
-
// ── sweny e2e ────────────────────────────────────────────────────────
|
|
79
|
-
const e2eCmd = program.command("e2e").description("End-to-end browser testing");
|
|
80
|
-
e2eCmd
|
|
81
|
-
.command("run [file]")
|
|
82
|
-
.description("Run e2e workflow files from .sweny/e2e/")
|
|
83
|
-
.option("--timeout <ms>", "Timeout per workflow in milliseconds (default: 900000 = 15 min)")
|
|
84
|
-
.action(async (file, options) => {
|
|
85
|
-
await runE2eRun({
|
|
86
|
-
file,
|
|
87
|
-
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
78
|
// ── sweny check ───────────────────────────────────────────────────────
|
|
91
79
|
program
|
|
92
80
|
.command("check")
|
|
@@ -462,7 +450,9 @@ implementCmd.action(async (issueId, options) => {
|
|
|
462
450
|
sourceControlProvider: options.sourceControlProvider || fileConfig["source-control-provider"] || "github",
|
|
463
451
|
codingAgentProvider: options.codingAgentProvider || fileConfig["coding-agent-provider"] || "claude",
|
|
464
452
|
dryRun: Boolean(options.dryRun),
|
|
465
|
-
|
|
453
|
+
// parsePositiveInt mirrors the triage path: junk → NaN, which
|
|
454
|
+
// validateInputs rejects via validateIntegerBound below.
|
|
455
|
+
maxImplementTurns: parsePositiveInt(options.maxImplementTurns ?? fileConfig["max-implement-turns"], 40),
|
|
466
456
|
baseBranch: options.baseBranch || fileConfig["base-branch"] || "main",
|
|
467
457
|
repository: options.repository || process.env.GITHUB_REPOSITORY || "",
|
|
468
458
|
outputDir: options.outputDir ||
|
|
@@ -470,6 +460,14 @@ implementCmd.action(async (issueId, options) => {
|
|
|
470
460
|
fileConfig["output-dir"] ||
|
|
471
461
|
".sweny/output",
|
|
472
462
|
};
|
|
463
|
+
// Validate (parity with triage): surface the curated "Missing: ..." errors
|
|
464
|
+
// and reject malformed integer flags before any execution.
|
|
465
|
+
const implErrors = validateInputs(config);
|
|
466
|
+
if (implErrors.length > 0) {
|
|
467
|
+
console.error(formatValidationErrors(implErrors));
|
|
468
|
+
console.error(c.subtle("\n Run sweny implement --help for usage information.\n"));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
473
471
|
const implementSkillDiscovery = configuredSkillsWithDiagnostics(process.env, process.cwd());
|
|
474
472
|
for (const w of implementSkillDiscovery.warnings) {
|
|
475
473
|
console.error(chalk.yellow(` ⚠ ${w.message}`));
|
|
@@ -611,6 +609,16 @@ export function loadWorkflowFile(filePath, knownSkills) {
|
|
|
611
609
|
return result.workflow;
|
|
612
610
|
}
|
|
613
611
|
export async function workflowRunAction(file, options) {
|
|
612
|
+
// No file given → batch-run the e2e workflows in .sweny/e2e/. This is the
|
|
613
|
+
// home for what used to be `sweny e2e run`; it lists what will run and
|
|
614
|
+
// confirms first (bypass with --yes).
|
|
615
|
+
if (!file) {
|
|
616
|
+
await runE2eRun({
|
|
617
|
+
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
|
618
|
+
yes: Boolean(options.yes),
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
614
622
|
// Discover skills first so the loader can flag UNKNOWN_SKILL at parse
|
|
615
623
|
// time. validateWorkflowSkills below still runs for richer category /
|
|
616
624
|
// env-var diagnostics, but the loader's structural check fires earlier
|
|
@@ -912,8 +920,10 @@ workflowCmd
|
|
|
912
920
|
.option("--json", "Output result as JSON")
|
|
913
921
|
.action(workflowValidateAction);
|
|
914
922
|
workflowCmd
|
|
915
|
-
.command("run
|
|
916
|
-
.description("Run a workflow from a YAML or JSON file")
|
|
923
|
+
.command("run [file]")
|
|
924
|
+
.description("Run a workflow from a YAML or JSON file. With no file, batch-runs every workflow in .sweny/e2e/ (lists and confirms first; use --yes to skip the prompt).")
|
|
925
|
+
.option("--timeout <ms>", "Per-workflow timeout in ms for batch runs (default: 900000 = 15 min)")
|
|
926
|
+
.option("-y, --yes", "Skip the batch confirmation prompt (for CI)")
|
|
917
927
|
.option("--dry-run", "Execute until the first conditional routing decision, then stop (no side effects past that point). NOTE: behavior changed in this release — previously --dry-run only printed the node list. For that behavior use --list-nodes.")
|
|
918
928
|
.option("--list-nodes", "Validate, print nodes/skills, and exit without running. (Replaces the pre-Fix-#6 --dry-run behavior.)")
|
|
919
929
|
.option("--json", "Output result as JSON on stdout; suppress progress output")
|
package/dist/cli/new.js
CHANGED
|
@@ -468,6 +468,13 @@ function cancel() {
|
|
|
468
468
|
*/
|
|
469
469
|
export async function runNew(options) {
|
|
470
470
|
const cwd = process.cwd();
|
|
471
|
+
// ── E2E shortcut: `sweny new e2e` jumps straight into the e2e wizard ──
|
|
472
|
+
// `new` is the single entry for all workflow creation; this is a named
|
|
473
|
+
// shortcut into the same flow the interactive picker reaches via "__e2e".
|
|
474
|
+
if (options?.marketplaceId === "e2e") {
|
|
475
|
+
await runE2eInit({ skipIntro: options.skipIntro });
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
471
478
|
// ── Marketplace install fast path ───────────────────────────────────
|
|
472
479
|
if (options?.marketplaceId) {
|
|
473
480
|
const { installMarketplaceWorkflow } = await import("./marketplace.js");
|
package/dist/eval/value.js
CHANGED
|
@@ -115,7 +115,18 @@ export function checkOutputRequired(paths, data) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
if (r.mode === "all") {
|
|
118
|
-
|
|
118
|
+
// An empty wildcard expansion (e.g. `findings[*].severity` when
|
|
119
|
+
// `findings` is `[]`) resolves to zero values. That is a
|
|
120
|
+
// structurally-absent required output, not a satisfied gate: treat it
|
|
121
|
+
// as a failure rather than vacuously passing. A non-wildcard path always
|
|
122
|
+
// yields exactly one value, so this branch only ever sees an empty array
|
|
123
|
+
// for an empty wildcard expansion. Same class as the route-eval
|
|
124
|
+
// `missing` warning: a required field that is absent must not green-light
|
|
125
|
+
// the gate.
|
|
126
|
+
if (r.values.length === 0) {
|
|
127
|
+
failures.push(`'${path}' resolved to no elements (empty wildcard expansion)`);
|
|
128
|
+
}
|
|
129
|
+
else if (!r.values.every((v) => v !== null && v !== undefined)) {
|
|
119
130
|
failures.push(`'${path}' resolved to null/undefined value(s)`);
|
|
120
131
|
}
|
|
121
132
|
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -32,7 +32,23 @@ export interface ExecuteOptions {
|
|
|
32
32
|
fetchAuth?: Record<string, string>;
|
|
33
33
|
/** When true, URL Sources throw instead of fetching */
|
|
34
34
|
offline?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Workflow-level hard cap on total node executions (the number of times the
|
|
37
|
+
* main loop visits a node). Guards against unbounded cycles, e.g. a back-edge
|
|
38
|
+
* with no `max_iterations` or an LLM route evaluator that keeps choosing the
|
|
39
|
+
* loop-back edge. Defaults to {@link DEFAULT_MAX_STEPS}. When exceeded the
|
|
40
|
+
* executor throws a clear "step budget exceeded" error rather than hanging.
|
|
41
|
+
* Per-edge `max_iterations` behavior is unchanged.
|
|
42
|
+
*/
|
|
43
|
+
max_steps?: number;
|
|
35
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Default workflow-level step budget. Counts total node executions across the
|
|
47
|
+
* whole run (loop iterations included). Generous enough for legitimate deep
|
|
48
|
+
* workflows with controlled cycles, tight enough to terminate a runaway loop
|
|
49
|
+
* before it burns meaningful model spend.
|
|
50
|
+
*/
|
|
51
|
+
export declare const DEFAULT_MAX_STEPS = 200;
|
|
36
52
|
/**
|
|
37
53
|
* Execute a workflow from entry to completion.
|
|
38
54
|
*
|