@sweny-ai/core 0.1.123 → 0.1.124
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 +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +187 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- package/package.json +1 -1
package/dist/cli/config-file.js
CHANGED
|
@@ -32,6 +32,40 @@ export function loadDotenv(cwd = process.cwd()) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* `.sweny.yml` keys for the agent execution floor (#360) and the env var each
|
|
37
|
+
* maps to. The runtime (claude.ts / agent-env.ts) reads the env var, so the
|
|
38
|
+
* same knob works for the CLI, the Action and library callers.
|
|
39
|
+
*/
|
|
40
|
+
const AGENT_FILE_KEYS = [
|
|
41
|
+
[["env-passthrough", "env_passthrough"], "SWENY_ENV_PASSTHROUGH"],
|
|
42
|
+
[["env-scope", "env_scope"], "SWENY_ENV_SCOPE"],
|
|
43
|
+
[["sandbox"], "SWENY_SANDBOX"],
|
|
44
|
+
[["sandbox-allowed-domains", "sandbox_allowed_domains"], "SWENY_SANDBOX_ALLOWED_DOMAINS"],
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Copy agent sandbox / env-passthrough settings from `.sweny.yml` into the
|
|
48
|
+
* environment. Real env vars win (same precedence as {@link loadDotenv}).
|
|
49
|
+
* YAML booleans arrive as "true"/"false", which the sandbox mode parser
|
|
50
|
+
* treats as on/off.
|
|
51
|
+
*/
|
|
52
|
+
export function applyAgentFileConfig(fileConfig, env = process.env) {
|
|
53
|
+
for (const [keys, envVar] of AGENT_FILE_KEYS) {
|
|
54
|
+
if (env[envVar] !== undefined)
|
|
55
|
+
continue;
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const v = fileConfig[key];
|
|
58
|
+
if (Array.isArray(v) && v.length > 0) {
|
|
59
|
+
env[envVar] = v.join(",");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (typeof v === "string" && v) {
|
|
63
|
+
env[envVar] = v;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
35
69
|
/**
|
|
36
70
|
* Search upward from `cwd` for `.sweny.yml` and parse it.
|
|
37
71
|
* Scalar values are strings, list values (rules, context) are string arrays.
|
|
@@ -114,7 +148,7 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
114
148
|
# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file
|
|
115
149
|
# issue-tracker-provider: github-issues # github-issues | linear | jira
|
|
116
150
|
# source-control-provider: github # github | gitlab
|
|
117
|
-
# coding-agent-provider: claude # claude
|
|
151
|
+
# coding-agent-provider: claude # claude (the only supported agent)
|
|
118
152
|
# notification-provider: console # console | slack | teams | discord | email | webhook
|
|
119
153
|
|
|
120
154
|
# ── Investigation ────────────────────────────────────────────────────
|
|
@@ -141,6 +175,13 @@ export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
|
141
175
|
# cloud-token: sweny_pk_...
|
|
142
176
|
# Or set SWENY_CLOUD_TOKEN in your environment.
|
|
143
177
|
|
|
178
|
+
# ── Agent sandbox ─────────────────────────────────────────────────────
|
|
179
|
+
# See https://docs.sweny.ai/advanced/agent-sandbox/
|
|
180
|
+
# sandbox: off # off (local default) | auto (CI default: sandbox if supported, else warn) | strict
|
|
181
|
+
# sandbox-allowed-domains: [internal.example.com] # extra hosts agent commands may reach
|
|
182
|
+
# env-scope: off # off (local default: full env) | on (CI default: allowlist)
|
|
183
|
+
# env-passthrough: [NPM_TOKEN] # extra env vars the agent may see when scoped ("*" = all)
|
|
184
|
+
|
|
144
185
|
# ── MCP servers ───────────────────────────────────────────────────────
|
|
145
186
|
# Extend the coding agent with additional tools via MCP.
|
|
146
187
|
# Value is a JSON object — each key is a server name you choose.
|
package/dist/cli/config.js
CHANGED
|
@@ -6,7 +6,7 @@ export function registerTriageCommand(program) {
|
|
|
6
6
|
return program
|
|
7
7
|
.command("triage")
|
|
8
8
|
.description("Run the SWEny triage workflow")
|
|
9
|
-
.option("--agent <provider>", "Coding agent: claude (
|
|
9
|
+
.option("--agent <provider>", "Coding agent: claude (the only supported agent)")
|
|
10
10
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
11
11
|
.option("--observability-provider <provider>", "Observability provider (default: none)")
|
|
12
12
|
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
@@ -228,18 +228,12 @@ export function validateInputs(config) {
|
|
|
228
228
|
errors.push("Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN — get a key at https://console.anthropic.com");
|
|
229
229
|
}
|
|
230
230
|
break;
|
|
231
|
-
case "codex":
|
|
232
|
-
if (!config.openaiApiKey) {
|
|
233
|
-
errors.push("Missing: OPENAI_API_KEY — get a key at https://platform.openai.com/api-keys");
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
case "gemini":
|
|
237
|
-
if (!config.geminiApiKey) {
|
|
238
|
-
errors.push("Missing: GEMINI_API_KEY or GOOGLE_API_KEY — get a key at https://aistudio.google.com/app/apikey");
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
241
231
|
default:
|
|
242
|
-
|
|
232
|
+
// Honest --agent (#330): SWEny runs headless Claude Code only. Nothing
|
|
233
|
+
// dispatches on other values, so accepting them would silently run
|
|
234
|
+
// Claude under a different name.
|
|
235
|
+
errors.push(`Unsupported coding agent "${config.codingAgentProvider}": the only supported agent is "claude" ` +
|
|
236
|
+
`(headless Claude Code). Remove --agent / coding-agent-provider or set it to claude.`);
|
|
243
237
|
}
|
|
244
238
|
// Repository required unless all providers are file-based
|
|
245
239
|
const allLocal = config.issueTrackerProvider === "file" && config.sourceControlProvider === "file";
|
|
@@ -656,7 +650,7 @@ export function registerImplementCommand(program) {
|
|
|
656
650
|
return program
|
|
657
651
|
.command("implement <issueId>")
|
|
658
652
|
.description("Implement a fix for a specific issue and open a PR")
|
|
659
|
-
.option("--agent <provider>", "Coding agent: claude (
|
|
653
|
+
.option("--agent <provider>", "Coding agent: claude (the only supported agent)")
|
|
660
654
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
661
655
|
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
662
656
|
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
|
@@ -701,7 +695,10 @@ function parseMcpServers(json) {
|
|
|
701
695
|
}
|
|
702
696
|
function detectRepository() {
|
|
703
697
|
try {
|
|
704
|
-
const remote = execSync("git remote get-url origin", {
|
|
698
|
+
const remote = execSync("git remote get-url origin", {
|
|
699
|
+
encoding: "utf-8",
|
|
700
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
701
|
+
}).trim();
|
|
705
702
|
const match = remote.match(/[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
706
703
|
return match?.[1] ?? "";
|
|
707
704
|
}
|
package/dist/cli/e2e.d.ts
CHANGED
|
@@ -75,6 +75,28 @@ export interface E2eInitOptions {
|
|
|
75
75
|
* then generates .sweny/e2e/*.yml and .env additions.
|
|
76
76
|
*/
|
|
77
77
|
export declare function runE2eInit(options?: E2eInitOptions): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Default wall-clock budget (60 min) for a single workflow run when the
|
|
80
|
+
* caller doesn't pass `--timeout`. `--timeout 0` disables it. Shared by the e2e batch runner and the primary
|
|
81
|
+
* `sweny workflow run <file>` path (see main.ts workflowRunAction) so both
|
|
82
|
+
* fail loudly on a wedged node instead of hanging CI indefinitely. See #325.
|
|
83
|
+
*/
|
|
84
|
+
export declare const DEFAULT_WORKFLOW_TIMEOUT_MS: number;
|
|
85
|
+
export declare function withTimeout<T>(promise: Promise<T>, ms: number, label: string, onTimeout?: () => void): Promise<T>;
|
|
86
|
+
/**
|
|
87
|
+
* Run a workflow execution under a whole-run wall-clock budget, aborting the
|
|
88
|
+
* in-flight run via `AbortSignal` when the budget is exceeded.
|
|
89
|
+
*
|
|
90
|
+
* Shared by the `.sweny/e2e/` batch runner (below) and the primary
|
|
91
|
+
* `sweny workflow run <file>` path (main.ts `workflowRunAction`) so both
|
|
92
|
+
* paths get the same abort/timeout behavior from one implementation instead
|
|
93
|
+
* of main.ts re-deriving it. Before #325, `workflowRunAction` never wired
|
|
94
|
+
* `--timeout` or an abort signal into `execute()` at all: a wedged node
|
|
95
|
+
* (hung tool call, runaway model turn) could hang the primary run path
|
|
96
|
+
* indefinitely even though the abort/timeout plumbing already existed in
|
|
97
|
+
* the executor and `claude.run`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runWithWallClockBudget<T>(run: (signal: AbortSignal) => Promise<T>, timeoutMs: number, label: string): Promise<T>;
|
|
78
100
|
export interface E2eRunOptions {
|
|
79
101
|
file?: string;
|
|
80
102
|
timeout?: number;
|
package/dist/cli/e2e.js
CHANGED
|
@@ -481,6 +481,7 @@ export function buildFlowWorkflow(flow, baseUrl, cleanup) {
|
|
|
481
481
|
id: `e2e-${flow.type}`,
|
|
482
482
|
name: `E2E: ${typeName}`,
|
|
483
483
|
description: `End-to-end test for ${flow.type} flow`,
|
|
484
|
+
workflow_type: "e2e_test",
|
|
484
485
|
entry: "setup",
|
|
485
486
|
nodes,
|
|
486
487
|
edges,
|
|
@@ -760,7 +761,14 @@ export async function runE2eInit(options = {}) {
|
|
|
760
761
|
}
|
|
761
762
|
}
|
|
762
763
|
// ── Timeout helper ─────────────────────────────────────────────────────
|
|
763
|
-
|
|
764
|
+
/**
|
|
765
|
+
* Default wall-clock budget (60 min) for a single workflow run when the
|
|
766
|
+
* caller doesn't pass `--timeout`. `--timeout 0` disables it. Shared by the e2e batch runner and the primary
|
|
767
|
+
* `sweny workflow run <file>` path (see main.ts workflowRunAction) so both
|
|
768
|
+
* fail loudly on a wedged node instead of hanging CI indefinitely. See #325.
|
|
769
|
+
*/
|
|
770
|
+
export const DEFAULT_WORKFLOW_TIMEOUT_MS = 60 * 60 * 1000;
|
|
771
|
+
export function withTimeout(promise, ms, label, onTimeout) {
|
|
764
772
|
return new Promise((resolve, reject) => {
|
|
765
773
|
const timer = setTimeout(() => {
|
|
766
774
|
// Abort the underlying work first so it stops advancing and the in-flight
|
|
@@ -775,6 +783,26 @@ function withTimeout(promise, ms, label, onTimeout) {
|
|
|
775
783
|
.finally(() => clearTimeout(timer));
|
|
776
784
|
});
|
|
777
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Run a workflow execution under a whole-run wall-clock budget, aborting the
|
|
788
|
+
* in-flight run via `AbortSignal` when the budget is exceeded.
|
|
789
|
+
*
|
|
790
|
+
* Shared by the `.sweny/e2e/` batch runner (below) and the primary
|
|
791
|
+
* `sweny workflow run <file>` path (main.ts `workflowRunAction`) so both
|
|
792
|
+
* paths get the same abort/timeout behavior from one implementation instead
|
|
793
|
+
* of main.ts re-deriving it. Before #325, `workflowRunAction` never wired
|
|
794
|
+
* `--timeout` or an abort signal into `execute()` at all: a wedged node
|
|
795
|
+
* (hung tool call, runaway model turn) could hang the primary run path
|
|
796
|
+
* indefinitely even though the abort/timeout plumbing already existed in
|
|
797
|
+
* the executor and `claude.run`.
|
|
798
|
+
*/
|
|
799
|
+
export async function runWithWallClockBudget(run, timeoutMs, label) {
|
|
800
|
+
const controller = new AbortController();
|
|
801
|
+
// timeoutMs <= 0 means no wall-clock budget (`--timeout 0`).
|
|
802
|
+
if (!(timeoutMs > 0))
|
|
803
|
+
return run(controller.signal);
|
|
804
|
+
return withTimeout(run(controller.signal), timeoutMs, label, () => controller.abort());
|
|
805
|
+
}
|
|
778
806
|
/**
|
|
779
807
|
* Policy for a no-file batch run. Pure so it can be unit-tested without
|
|
780
808
|
* touching the filesystem, stdin, or the engine.
|
|
@@ -794,7 +822,7 @@ export function batchRunDecision(opts) {
|
|
|
794
822
|
*/
|
|
795
823
|
export async function runE2eRun(options) {
|
|
796
824
|
const cwd = process.cwd();
|
|
797
|
-
const timeoutMs = options.timeout
|
|
825
|
+
const timeoutMs = options.timeout ?? DEFAULT_WORKFLOW_TIMEOUT_MS;
|
|
798
826
|
// 1. Discover workflow files
|
|
799
827
|
let files;
|
|
800
828
|
if (options.file) {
|
|
@@ -911,9 +939,8 @@ export async function runE2eRun(options) {
|
|
|
911
939
|
}
|
|
912
940
|
}
|
|
913
941
|
};
|
|
914
|
-
const controller = new AbortController();
|
|
915
942
|
try {
|
|
916
|
-
const { results } = await
|
|
943
|
+
const { results } = await runWithWallClockBudget((signal) => execute(workflow, { run_id: vars.run_id, base_url: vars.base_url }, { skills, claude, observer, logger: consoleLogger, signal }), timeoutMs, `Workflow ${workflow.name}`);
|
|
917
944
|
// Extract report
|
|
918
945
|
const report = results.get("report");
|
|
919
946
|
if (report?.data) {
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare function workflowRunAction(file: string | undefined, options: Rec
|
|
|
7
7
|
mermaid?: boolean;
|
|
8
8
|
verbose?: boolean;
|
|
9
9
|
timeout?: string;
|
|
10
|
+
maxSteps?: string;
|
|
10
11
|
yes?: boolean;
|
|
11
12
|
}): Promise<void>;
|
|
12
13
|
export declare function workflowExportAction(name: string): void;
|
package/dist/cli/main.js
CHANGED
|
@@ -10,26 +10,29 @@ import { execute } from "../executor.js";
|
|
|
10
10
|
import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
|
|
11
11
|
import { consoleLogger } from "../types.js";
|
|
12
12
|
import { ClaudeClient } from "../claude.js";
|
|
13
|
-
import { createSkillMap, validateWorkflowSkills } from "../skills/index.js";
|
|
13
|
+
import { builtinSkills, createSkillMap, validateWorkflowSkills } from "../skills/index.js";
|
|
14
|
+
import { formatMissingSkillLines, skillEnvWarnings } from "./skill-env.js";
|
|
14
15
|
import { configuredSkills, configuredSkillsWithDiagnostics } from "../skills/custom-loader.js";
|
|
15
16
|
import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
|
|
16
17
|
import { loadAdditionalContext } from "../templates.js";
|
|
17
18
|
import { loadAndValidateWorkflow } from "../loader.js";
|
|
18
19
|
import { validateRuntimeInput } from "../inputs.js";
|
|
20
|
+
import { mergeDryRunIntoInput, parseRunBudgetFlags } from "./workflow-input.js";
|
|
19
21
|
import { stringify as stringifyYaml } from "yaml";
|
|
20
22
|
import { buildWorkflow, refineWorkflow } from "../workflow-builder.js";
|
|
21
23
|
import { toMermaidBlock } from "../mermaid.js";
|
|
22
24
|
import { runWorkflowDiagram } from "./diagram.js";
|
|
23
25
|
import { DagRenderer } from "./renderer.js";
|
|
24
26
|
import * as readline from "node:readline";
|
|
25
|
-
import { loadDotenv, loadConfigFile } from "./config-file.js";
|
|
27
|
+
import { loadDotenv, loadConfigFile, applyAgentFileConfig } from "./config-file.js";
|
|
26
28
|
import { buildCredentialMap } from "./credentials.js";
|
|
27
|
-
import { runNew } from "./new.js";
|
|
28
|
-
import { runE2eRun } from "./e2e.js";
|
|
29
|
+
import { nonInteractiveUsage, runNew } from "./new.js";
|
|
30
|
+
import { runE2eRun, runWithWallClockBudget, DEFAULT_WORKFLOW_TIMEOUT_MS } from "./e2e.js";
|
|
29
31
|
import { createVerboseToolObserver } from "./verbose-observer.js";
|
|
32
|
+
import { createRunLogger, renderReceiptLine, summarizeRun, writeStepSummary, WORKFLOW_RUN_DESCRIPTION, WORKFLOW_RUN_OPTIONS, } from "./run-output.js";
|
|
30
33
|
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, parsePositiveInt, } from "./config.js";
|
|
31
34
|
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatDagResultMarkdown, formatResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
32
|
-
import { checkProviderConnectivity } from "./check.js";
|
|
35
|
+
import { checkProviderConnectivity, detectClaudeCodeLogin, discoverWorkflowSkillIds, validateCheckInputs, } from "./check.js";
|
|
33
36
|
import { registerSetupCommand } from "./setup.js";
|
|
34
37
|
import { registerPublishCommand } from "./publish.js";
|
|
35
38
|
import { registerSkillCommand } from "./skill.js";
|
|
@@ -64,6 +67,8 @@ function composeObservers(...observers) {
|
|
|
64
67
|
}
|
|
65
68
|
// Auto-load .env before Commander parses (so env vars are available for defaults)
|
|
66
69
|
loadDotenv();
|
|
70
|
+
// Agent sandbox / env-passthrough keys from .sweny.yml -> SWENY_* env (#360).
|
|
71
|
+
applyAgentFileConfig(loadConfigFile());
|
|
67
72
|
const program = new Command()
|
|
68
73
|
.name("sweny")
|
|
69
74
|
.description("SWEny CLI \u2014 autonomous engineering workflows")
|
|
@@ -71,9 +76,17 @@ const program = new Command()
|
|
|
71
76
|
// ── sweny new ─────────────────────────────────────────────────────────
|
|
72
77
|
program
|
|
73
78
|
.command("new [id]")
|
|
74
|
-
.description("Create a new workflow. With no id, opens the interactive picker. With an id,
|
|
75
|
-
.
|
|
76
|
-
|
|
79
|
+
.description("Create a new workflow. With no id, opens the interactive picker. With an id, uses that built-in template, or installs it from the marketplace (swenyai/workflows).")
|
|
80
|
+
.option("--template <id>", "Use a built-in template without the picker")
|
|
81
|
+
.option("-y, --yes", "Skip every prompt (never overwrites existing files)")
|
|
82
|
+
.action(async (id, options) => {
|
|
83
|
+
// Prompts need a terminal. Without one (CI, pipes), a prompt never
|
|
84
|
+
// settles, so print usage and exit 2 instead of hanging.
|
|
85
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
86
|
+
console.error(nonInteractiveUsage());
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
await runNew({ marketplaceId: id, template: options.template, yes: options.yes });
|
|
77
90
|
});
|
|
78
91
|
// ── sweny check ───────────────────────────────────────────────────────
|
|
79
92
|
program
|
|
@@ -82,13 +95,16 @@ program
|
|
|
82
95
|
.action(async () => {
|
|
83
96
|
const fileConfig = loadConfigFile();
|
|
84
97
|
const config = parseCliInputs({}, fileConfig);
|
|
85
|
-
const
|
|
98
|
+
const workflows = discoverWorkflowSkillIds(process.cwd());
|
|
99
|
+
const scope = workflows.found ? workflows.skillIds : undefined;
|
|
100
|
+
const claudeCodeLogin = detectClaudeCodeLogin();
|
|
101
|
+
const errors = validateCheckInputs(config, { scope, claudeCodeLogin });
|
|
86
102
|
if (errors.length > 0) {
|
|
87
103
|
console.error(formatValidationErrors(errors));
|
|
88
104
|
process.exit(1);
|
|
89
105
|
}
|
|
90
106
|
console.log(chalk.dim("\n Checking provider connectivity…\n"));
|
|
91
|
-
const results = await checkProviderConnectivity(config);
|
|
107
|
+
const results = await checkProviderConnectivity(config, { scope, claudeCodeLogin });
|
|
92
108
|
console.log(formatCheckResults(results));
|
|
93
109
|
const hasFailure = results.some((r) => r.status === "fail");
|
|
94
110
|
process.exit(hasFailure ? 1 : 0);
|
|
@@ -418,17 +434,33 @@ triageCmd.action(async (options) => {
|
|
|
418
434
|
process.exit(hasFailed ? 1 : 0);
|
|
419
435
|
}
|
|
420
436
|
catch (error) {
|
|
437
|
+
const crashMsg = error instanceof Error ? error.message : "Unknown error";
|
|
421
438
|
if (config.json) {
|
|
422
|
-
console.log(JSON.stringify({ error:
|
|
439
|
+
console.log(JSON.stringify({ error: crashMsg }));
|
|
423
440
|
}
|
|
424
441
|
else {
|
|
425
442
|
console.error(formatCrashError(error));
|
|
426
443
|
}
|
|
444
|
+
// Finalize the cloud run as failed. execute() threw (including the
|
|
445
|
+
// RouteEvaluationError the executor raises on route-eval failure), so
|
|
446
|
+
// the in-try finish never ran and the run would otherwise stay stuck at
|
|
447
|
+
// "running" forever. No results map exists on this path, so pass an empty
|
|
448
|
+
// one; the error summary is a short message, never raw agent prose.
|
|
449
|
+
try {
|
|
450
|
+
// PRIVACY: pass the raw error, not crashMsg. finishCloudLifecycle reduces it
|
|
451
|
+
// to error.name + a 200-char message: thrown messages can embed agent/LLM
|
|
452
|
+
// prose or log text, and the cloud gets metadata only. It also sends at
|
|
453
|
+
// most one finish per run, so a throw after the in-try finish cannot
|
|
454
|
+
// overwrite the real result with a second "failed".
|
|
455
|
+
await finishCloudLifecycle(config, cloudHandle, new Map(), Date.now() - runStart, "failed", error);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// silent — cloud reporting must never block or mask the crash
|
|
459
|
+
}
|
|
427
460
|
// Best-effort GitHub Actions step summary on crash
|
|
428
461
|
if (config.notificationProvider === "github-summary" && process.env.GITHUB_STEP_SUMMARY) {
|
|
429
462
|
try {
|
|
430
|
-
|
|
431
|
-
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## ❌ SWEny Triage Crashed\n\n\`\`\`\n${msg}\n\`\`\`\n`);
|
|
463
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## ❌ SWEny Triage Crashed\n\n\`\`\`\n${crashMsg}\n\`\`\`\n`);
|
|
432
464
|
}
|
|
433
465
|
catch {
|
|
434
466
|
// ignore
|
|
@@ -589,7 +621,22 @@ implementCmd.action(async (issueId, options) => {
|
|
|
589
621
|
process.exit(0);
|
|
590
622
|
}
|
|
591
623
|
catch (err) {
|
|
592
|
-
|
|
624
|
+
const crashMsg = err instanceof Error ? err.message : String(err);
|
|
625
|
+
console.error(chalk.red(`\n Error: ${crashMsg}\n`));
|
|
626
|
+
// Finalize the cloud run as failed (covers thrown errors, incl.
|
|
627
|
+
// RouteEvaluationError). Without this a crashed implement run stays
|
|
628
|
+
// "running" in cloud forever.
|
|
629
|
+
try {
|
|
630
|
+
// PRIVACY: pass the raw error, not crashMsg. finishCloudLifecycle reduces it
|
|
631
|
+
// to error.name + a 200-char message: thrown messages can embed agent/LLM
|
|
632
|
+
// prose or log text, and the cloud gets metadata only. It also sends at
|
|
633
|
+
// most one finish per run, so a throw after the in-try finish cannot
|
|
634
|
+
// overwrite the real result with a second "failed".
|
|
635
|
+
await finishCloudLifecycle(config, implCloudHandle, new Map(), Date.now() - implRunStart, "failed", err);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
// silent
|
|
639
|
+
}
|
|
593
640
|
process.exit(1);
|
|
594
641
|
}
|
|
595
642
|
});
|
|
@@ -612,12 +659,22 @@ export function loadWorkflowFile(filePath, knownSkills) {
|
|
|
612
659
|
return result.workflow;
|
|
613
660
|
}
|
|
614
661
|
export async function workflowRunAction(file, options) {
|
|
662
|
+
// Reject junk --timeout/--max-steps up front (both paths) instead of
|
|
663
|
+
// silently falling back to a default.
|
|
664
|
+
let budget;
|
|
665
|
+
try {
|
|
666
|
+
budget = parseRunBudgetFlags(options.timeout, options.maxSteps, DEFAULT_WORKFLOW_TIMEOUT_MS);
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
console.error(chalk.red(`\n ${err instanceof Error ? err.message : String(err)}\n`));
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
615
672
|
// No file given → batch-run the e2e workflows in .sweny/e2e/. This is the
|
|
616
673
|
// home for what used to be `sweny e2e run`; it lists what will run and
|
|
617
674
|
// confirms first (bypass with --yes).
|
|
618
675
|
if (!file) {
|
|
619
676
|
await runE2eRun({
|
|
620
|
-
timeout: options.timeout ?
|
|
677
|
+
timeout: options.timeout === undefined ? undefined : budget.timeoutMs,
|
|
621
678
|
yes: Boolean(options.yes),
|
|
622
679
|
});
|
|
623
680
|
return;
|
|
@@ -631,7 +688,9 @@ export async function workflowRunAction(file, options) {
|
|
|
631
688
|
for (const w of earlySkillDiscovery.warnings) {
|
|
632
689
|
console.error(chalk.yellow(` ⚠ ${w.message}`));
|
|
633
690
|
}
|
|
634
|
-
|
|
691
|
+
// A built-in skill with unset env is not "unknown": include every built-in id
|
|
692
|
+
// so the loader only flags genuine typos; missing env is reported below.
|
|
693
|
+
const knownSkillIds = new Set([...earlySkillDiscovery.skills.map((s) => s.id), ...builtinSkills.map((s) => s.id)]);
|
|
635
694
|
let workflow;
|
|
636
695
|
try {
|
|
637
696
|
workflow = loadWorkflowFile(file, knownSkillIds);
|
|
@@ -663,23 +722,16 @@ export async function workflowRunAction(file, options) {
|
|
|
663
722
|
// we require at least one configured skill per category.
|
|
664
723
|
const validation = validateWorkflowSkills(workflow, skills, workflow.skills);
|
|
665
724
|
if (validation.errors.length > 0) {
|
|
666
|
-
console.error(chalk.red(`\n Workflow cannot run
|
|
667
|
-
for (const
|
|
668
|
-
console.error(chalk.red(` \u2717 ${
|
|
725
|
+
console.error(chalk.red(`\n Workflow cannot run:\n`));
|
|
726
|
+
for (const line of formatMissingSkillLines(validation))
|
|
727
|
+
console.error(chalk.red(` \u2717 ${line}`));
|
|
669
728
|
const unknown = validation.missing.filter((m) => m.category === "unknown");
|
|
670
|
-
const envGaps = validation.missing.filter((m) => m.category !== "unknown" && m.missingEnv.length > 0);
|
|
671
729
|
if (unknown.length > 0) {
|
|
672
730
|
console.error(chalk.dim(`\n These skill IDs aren't built-in or discovered in .{claude,sweny,agents,gemini}/skills/.\n Scaffold one with:\n`));
|
|
673
731
|
for (const m of unknown) {
|
|
674
732
|
console.error(chalk.dim(` sweny skill new ${m.id}`));
|
|
675
733
|
}
|
|
676
734
|
}
|
|
677
|
-
if (envGaps.length > 0) {
|
|
678
|
-
console.error(chalk.dim(`\n These skills are built-in but their env vars aren't set:`));
|
|
679
|
-
for (const m of envGaps) {
|
|
680
|
-
console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
735
|
console.error(chalk.dim(`\n Run \`sweny skill list\` to see what's available.\n`));
|
|
684
736
|
process.exit(1);
|
|
685
737
|
return;
|
|
@@ -698,10 +750,12 @@ export async function workflowRunAction(file, options) {
|
|
|
698
750
|
credentials: buildCredentialMap(),
|
|
699
751
|
userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
|
|
700
752
|
});
|
|
753
|
+
// Raw [info]/[debug] lines are verbose-only; warnings are rendered (#383).
|
|
754
|
+
const runLogger = createRunLogger({ verbose: Boolean(options.verbose), tty: isTTY });
|
|
701
755
|
const claude = new ClaudeClient({
|
|
702
756
|
maxTurns: config.maxInvestigateTurns || 50,
|
|
703
757
|
cwd: process.cwd(),
|
|
704
|
-
logger:
|
|
758
|
+
logger: runLogger,
|
|
705
759
|
mcpServers,
|
|
706
760
|
model: workflow.model,
|
|
707
761
|
});
|
|
@@ -733,6 +787,7 @@ export async function workflowRunAction(file, options) {
|
|
|
733
787
|
else {
|
|
734
788
|
process.stderr.write(` ${icon} ${event.node} ${elapsed}\n`);
|
|
735
789
|
}
|
|
790
|
+
runLogger.flush();
|
|
736
791
|
break;
|
|
737
792
|
}
|
|
738
793
|
case "workflow:end":
|
|
@@ -775,7 +830,7 @@ export async function workflowRunAction(file, options) {
|
|
|
775
830
|
process.exit(1);
|
|
776
831
|
return;
|
|
777
832
|
}
|
|
778
|
-
workflowInput = validated.value;
|
|
833
|
+
workflowInput = mergeDryRunIntoInput(validated.value, config.dryRun);
|
|
779
834
|
}
|
|
780
835
|
else {
|
|
781
836
|
workflowInput = {
|
|
@@ -827,18 +882,31 @@ export async function workflowRunAction(file, options) {
|
|
|
827
882
|
console.log(c.subtle(` cloud: ${wfCloudHandle.dashboardUrl}`));
|
|
828
883
|
}
|
|
829
884
|
const observer = composeObservers(wfProgressObserver, options.verbose ? createVerboseToolObserver() : undefined, options.stream ? createStreamObserver() : undefined, createCloudStreamObserver(config, wfCloudHandle));
|
|
885
|
+
// #325: this is the primary run path (GitHub Action / CLI / MCP all land
|
|
886
|
+
// here), and previously never passed signal/timeoutMs/max_steps to
|
|
887
|
+
// execute() at all: a wedged node (hung tool call, runaway model turn)
|
|
888
|
+
// could hang the run indefinitely despite the abort/timeout plumbing
|
|
889
|
+
// existing in the executor. --timeout is a whole-run wall-clock budget,
|
|
890
|
+
// enforced by runWithWallClockBudget (shared with the .sweny/e2e/ batch
|
|
891
|
+
// runner in e2e.ts). Absent = DEFAULT_WORKFLOW_TIMEOUT_MS (60 min);
|
|
892
|
+
// `--timeout 0` = no wall-clock budget; junk is rejected above.
|
|
893
|
+
// --max-steps overrides the executor's own DEFAULT_MAX_STEPS when set.
|
|
894
|
+
const wfTimeoutMs = budget.timeoutMs;
|
|
895
|
+
const wfMaxSteps = budget.maxSteps;
|
|
830
896
|
try {
|
|
831
|
-
const { results, trace } = await execute(workflow, workflowInput, {
|
|
897
|
+
const { results, trace } = await runWithWallClockBudget((signal) => execute(workflow, workflowInput, {
|
|
832
898
|
skills,
|
|
833
899
|
claude,
|
|
834
900
|
observer,
|
|
835
|
-
logger:
|
|
901
|
+
logger: runLogger,
|
|
836
902
|
cwd: process.cwd(),
|
|
837
903
|
env: process.env,
|
|
838
904
|
fetchAuth: config.fetchAuth,
|
|
839
905
|
offline: config.offline,
|
|
840
906
|
fileRoot: config.fileRoot || undefined,
|
|
841
|
-
|
|
907
|
+
signal,
|
|
908
|
+
max_steps: wfMaxSteps,
|
|
909
|
+
}), wfTimeoutMs, `Workflow ${workflow.name}`);
|
|
842
910
|
const wfDurationMs = Date.now() - runStart;
|
|
843
911
|
const wfHasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
844
912
|
// Close the cloud lifecycle session BEFORE the JSON early-exit so
|
|
@@ -870,16 +938,37 @@ export async function workflowRunAction(file, options) {
|
|
|
870
938
|
catch {
|
|
871
939
|
// silent
|
|
872
940
|
}
|
|
941
|
+
// Run receipt (metadata only) + optional $GITHUB_STEP_SUMMARY.
|
|
942
|
+
runLogger.flush();
|
|
943
|
+
const receipt = summarizeRun(results, wfDurationMs);
|
|
944
|
+
writeStepSummary(workflow, results, receipt, trace);
|
|
873
945
|
if (wfHasFailed) {
|
|
874
|
-
console.error(
|
|
946
|
+
console.error(` ${renderReceiptLine(receipt, isTTY)}\n`);
|
|
875
947
|
process.exit(1);
|
|
876
948
|
return;
|
|
877
949
|
}
|
|
878
|
-
console.log(
|
|
950
|
+
console.log(` ${renderReceiptLine(receipt, isTTY)}\n`);
|
|
879
951
|
process.exit(0);
|
|
880
952
|
}
|
|
881
953
|
catch (err) {
|
|
882
|
-
|
|
954
|
+
const crashMsg = err instanceof Error ? err.message : String(err);
|
|
955
|
+
console.error(chalk.red(`\n Error: ${crashMsg}\n`));
|
|
956
|
+
runLogger.flush();
|
|
957
|
+
console.error(` ${renderReceiptLine(summarizeRun(new Map(), Date.now() - runStart, true), isTTY)}\n`);
|
|
958
|
+
// Finalize the cloud run as failed (covers thrown errors, incl.
|
|
959
|
+
// RouteEvaluationError). Without this a crashed workflow run stays
|
|
960
|
+
// "running" in cloud forever.
|
|
961
|
+
try {
|
|
962
|
+
// PRIVACY: pass the raw error, not crashMsg. finishCloudLifecycle reduces it
|
|
963
|
+
// to error.name + a 200-char message: thrown messages can embed agent/LLM
|
|
964
|
+
// prose or log text, and the cloud gets metadata only. It also sends at
|
|
965
|
+
// most one finish per run, so a throw after the in-try finish cannot
|
|
966
|
+
// overwrite the real result with a second "failed".
|
|
967
|
+
await finishCloudLifecycle(config, wfCloudHandle, new Map(), Date.now() - runStart, "failed", err);
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
// silent
|
|
971
|
+
}
|
|
883
972
|
process.exit(1);
|
|
884
973
|
}
|
|
885
974
|
}
|
|
@@ -904,12 +993,21 @@ export function workflowExportAction(name) {
|
|
|
904
993
|
}
|
|
905
994
|
export function workflowValidateAction(file, options) {
|
|
906
995
|
const result = loadAndValidateWorkflow(file);
|
|
996
|
+
// Missing skill env is a warning here, not a failure: `run` is where it blocks.
|
|
997
|
+
const warnings = result.ok
|
|
998
|
+
? skillEnvWarnings(result.workflow, process.env, builtinSkills.concat(configuredSkills(process.env, process.cwd())))
|
|
999
|
+
: [];
|
|
907
1000
|
if (options.json) {
|
|
908
1001
|
const errs = result.ok ? [] : result.errors;
|
|
909
|
-
|
|
1002
|
+
const payload = { valid: result.ok, errors: errs };
|
|
1003
|
+
if (warnings.length > 0)
|
|
1004
|
+
payload.warnings = warnings;
|
|
1005
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
910
1006
|
}
|
|
911
1007
|
else if (result.ok) {
|
|
912
1008
|
console.log(chalk.green(` \u2713 ${file} is valid`));
|
|
1009
|
+
for (const w of warnings)
|
|
1010
|
+
console.error(chalk.yellow(` \u26A0 ${w}`));
|
|
913
1011
|
}
|
|
914
1012
|
else {
|
|
915
1013
|
console.error(chalk.red(` \u2717 ${file} has ${result.errors.length} validation error${result.errors.length > 1 ? "s" : ""}:`));
|
|
@@ -924,19 +1022,13 @@ workflowCmd
|
|
|
924
1022
|
.description("Validate a workflow YAML or JSON file")
|
|
925
1023
|
.option("--json", "Output result as JSON")
|
|
926
1024
|
.action(workflowValidateAction);
|
|
927
|
-
workflowCmd
|
|
1025
|
+
const workflowRunCmd = workflowCmd
|
|
928
1026
|
.command("run [file]")
|
|
929
|
-
.description(
|
|
930
|
-
.option("--timeout <ms>", "Per-workflow timeout in ms for batch runs (default: 900000 = 15 min)")
|
|
931
|
-
.option("-y, --yes", "Skip the batch confirmation prompt (for CI)")
|
|
932
|
-
.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.")
|
|
933
|
-
.option("--list-nodes", "Validate, print nodes/skills, and exit without running. (Replaces the pre-Fix-#6 --dry-run behavior.)")
|
|
934
|
-
.option("--json", "Output result as JSON on stdout; suppress progress output")
|
|
935
|
-
.option("--stream", "Stream NDJSON events to stdout (for Studio / automation)")
|
|
936
|
-
.option("--verbose", "Print each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.")
|
|
937
|
-
.option("--mermaid", "Output a Mermaid diagram with execution state after run")
|
|
938
|
-
.option("--input <json>", "JSON string of input data to pass to the workflow")
|
|
1027
|
+
.description(WORKFLOW_RUN_DESCRIPTION)
|
|
939
1028
|
.action(workflowRunAction);
|
|
1029
|
+
for (const [flags, description] of WORKFLOW_RUN_OPTIONS) {
|
|
1030
|
+
workflowRunCmd.option(flags, description);
|
|
1031
|
+
}
|
|
940
1032
|
workflowCmd
|
|
941
1033
|
.command("diagram <file>")
|
|
942
1034
|
.description("Render a workflow as a Mermaid diagram (raw .mmd by default; .md output auto-fences)")
|
|
@@ -24,7 +24,9 @@ export interface FetchedWorkflow {
|
|
|
24
24
|
yaml: string;
|
|
25
25
|
}
|
|
26
26
|
export declare function fetchMarketplaceWorkflow(id: string): Promise<FetchedWorkflow>;
|
|
27
|
-
export declare function fetchMarketplaceIndex(
|
|
27
|
+
export declare function fetchMarketplaceIndex(opts?: {
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}): Promise<MarketplaceEntry[]>;
|
|
28
30
|
export interface ProviderMismatch {
|
|
29
31
|
category: SkillCategory;
|
|
30
32
|
configKey: string;
|
package/dist/cli/marketplace.js
CHANGED
|
@@ -46,11 +46,11 @@ export async function fetchMarketplaceWorkflow(id) {
|
|
|
46
46
|
}
|
|
47
47
|
return { id, yaml: await res.text() };
|
|
48
48
|
}
|
|
49
|
-
export async function fetchMarketplaceIndex() {
|
|
49
|
+
export async function fetchMarketplaceIndex(opts = {}) {
|
|
50
50
|
const url = `${MARKETPLACE_RAW_BASE}/index.json`;
|
|
51
51
|
let res;
|
|
52
52
|
try {
|
|
53
|
-
res = await fetch(url);
|
|
53
|
+
res = await fetch(url, opts.timeoutMs ? { signal: AbortSignal.timeout(opts.timeoutMs) } : undefined);
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
56
56
|
const err = new Error(`Could not reach github.com`);
|
package/dist/cli/new.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Credential {
|
|
|
13
13
|
hint?: string;
|
|
14
14
|
url?: string;
|
|
15
15
|
default?: string;
|
|
16
|
+
/** Written commented-out in .env (not needed in every setup). */
|
|
17
|
+
optional?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface InitSelections {
|
|
18
20
|
sourceControl: string;
|
|
@@ -118,6 +120,13 @@ export declare function writeWorkflowFile(cwd: string, id: string, yaml: string,
|
|
|
118
120
|
exists?: boolean;
|
|
119
121
|
path: string;
|
|
120
122
|
};
|
|
123
|
+
/**
|
|
124
|
+
* Make sure `.env` is gitignored: create `.gitignore` if missing, append
|
|
125
|
+
* `.env` if absent. Returns what happened.
|
|
126
|
+
*/
|
|
127
|
+
export declare function ensureGitignoreEnv(cwd: string): "created" | "appended" | "present";
|
|
128
|
+
/** Usage printed (exit 2) when `sweny new` has no terminal and no --yes. */
|
|
129
|
+
export declare function nonInteractiveUsage(): string;
|
|
121
130
|
/**
|
|
122
131
|
* Interactive workflow creation wizard — workflow-first approach.
|
|
123
132
|
*
|
|
@@ -141,4 +150,10 @@ export declare function writeWorkflowFile(cwd: string, id: string, yaml: string,
|
|
|
141
150
|
export declare function runNew(options?: {
|
|
142
151
|
marketplaceId?: string;
|
|
143
152
|
skipIntro?: boolean;
|
|
153
|
+
/** Built-in template id: skip the picker. */
|
|
154
|
+
template?: string;
|
|
155
|
+
/** Skip every confirmation prompt (never overwrites existing files). */
|
|
156
|
+
yes?: boolean;
|
|
157
|
+
/** Internal: the id came from the marketplace picker, skip the built-in shortcut. */
|
|
158
|
+
forceMarketplace?: boolean;
|
|
144
159
|
}): Promise<void>;
|