@sweny-ai/core 0.1.0
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/__tests__/claude.test.d.ts +1 -0
- package/dist/__tests__/claude.test.js +328 -0
- package/dist/__tests__/executor.test.d.ts +1 -0
- package/dist/__tests__/executor.test.js +296 -0
- package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/datadog.integration.test.js +23 -0
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
- package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/github.integration.test.js +37 -0
- package/dist/__tests__/integration/harness.d.ts +24 -0
- package/dist/__tests__/integration/harness.js +34 -0
- package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/linear.integration.test.js +15 -0
- package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/sentry.integration.test.js +20 -0
- package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/slack.integration.test.js +22 -0
- package/dist/__tests__/schema.test.d.ts +1 -0
- package/dist/__tests__/schema.test.js +239 -0
- package/dist/__tests__/skills-index.test.d.ts +1 -0
- package/dist/__tests__/skills-index.test.js +122 -0
- package/dist/__tests__/skills.test.d.ts +1 -0
- package/dist/__tests__/skills.test.js +296 -0
- package/dist/__tests__/studio.test.d.ts +1 -0
- package/dist/__tests__/studio.test.js +172 -0
- package/dist/__tests__/testing.test.d.ts +1 -0
- package/dist/__tests__/testing.test.js +224 -0
- package/dist/browser.d.ts +17 -0
- package/dist/browser.js +22 -0
- package/dist/claude.d.ts +48 -0
- package/dist/claude.js +293 -0
- package/dist/cli/check.d.ts +11 -0
- package/dist/cli/check.js +237 -0
- package/dist/cli/config-file.d.ts +12 -0
- package/dist/cli/config-file.js +208 -0
- package/dist/cli/config.d.ts +77 -0
- package/dist/cli/config.js +565 -0
- package/dist/cli/main.d.ts +10 -0
- package/dist/cli/main.js +744 -0
- package/dist/cli/output.d.ts +26 -0
- package/dist/cli/output.js +357 -0
- package/dist/cli/renderer.d.ts +33 -0
- package/dist/cli/renderer.js +423 -0
- package/dist/cli/renderer.test.d.ts +1 -0
- package/dist/cli/renderer.test.js +302 -0
- package/dist/cli/setup.d.ts +11 -0
- package/dist/cli/setup.js +310 -0
- package/dist/executor.d.ts +29 -0
- package/dist/executor.js +173 -0
- package/dist/executor.test.d.ts +1 -0
- package/dist/executor.test.js +314 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +36 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +183 -0
- package/dist/mcp.test.d.ts +1 -0
- package/dist/mcp.test.js +334 -0
- package/dist/schema.d.ts +318 -0
- package/dist/schema.js +207 -0
- package/dist/skills/betterstack.d.ts +7 -0
- package/dist/skills/betterstack.js +114 -0
- package/dist/skills/datadog.d.ts +7 -0
- package/dist/skills/datadog.js +107 -0
- package/dist/skills/github.d.ts +8 -0
- package/dist/skills/github.js +155 -0
- package/dist/skills/index.d.ts +68 -0
- package/dist/skills/index.js +134 -0
- package/dist/skills/linear.d.ts +7 -0
- package/dist/skills/linear.js +89 -0
- package/dist/skills/notification.d.ts +11 -0
- package/dist/skills/notification.js +142 -0
- package/dist/skills/sentry.d.ts +7 -0
- package/dist/skills/sentry.js +105 -0
- package/dist/skills/slack.d.ts +8 -0
- package/dist/skills/slack.js +115 -0
- package/dist/studio.d.ts +124 -0
- package/dist/studio.js +174 -0
- package/dist/testing.d.ts +88 -0
- package/dist/testing.js +253 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +11 -0
- package/dist/workflow-builder.d.ts +45 -0
- package/dist/workflow-builder.js +120 -0
- package/dist/workflow-builder.test.d.ts +1 -0
- package/dist/workflow-builder.test.js +117 -0
- package/dist/workflows/implement.d.ts +11 -0
- package/dist/workflows/implement.js +83 -0
- package/dist/workflows/index.d.ts +2 -0
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/triage.d.ts +18 -0
- package/dist/workflows/triage.js +108 -0
- package/package.json +83 -0
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
const { version } = _require("../../package.json");
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { execute } from "../executor.js";
|
|
10
|
+
import { triageWorkflow } from "../workflows/triage.js";
|
|
11
|
+
import { implementWorkflow } from "../workflows/implement.js";
|
|
12
|
+
import { consoleLogger } from "../types.js";
|
|
13
|
+
import { ClaudeClient } from "../claude.js";
|
|
14
|
+
import { createSkillMap, configuredSkills } from "../skills/index.js";
|
|
15
|
+
import { validateWorkflow as validateWorkflowSchema } from "../schema.js";
|
|
16
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
17
|
+
import { buildWorkflow, refineWorkflow } from "../workflow-builder.js";
|
|
18
|
+
import { DagRenderer } from "./renderer.js";
|
|
19
|
+
import * as readline from "node:readline";
|
|
20
|
+
import { loadDotenv, loadConfigFile, STARTER_CONFIG } from "./config-file.js";
|
|
21
|
+
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, } from "./config.js";
|
|
22
|
+
import { c, formatBanner, getStepDetails, formatStepLine, formatDagResultHuman, formatResultJson, formatValidationErrors, formatCrashError, formatCheckResults, } from "./output.js";
|
|
23
|
+
import { checkProviderConnectivity } from "./check.js";
|
|
24
|
+
import { registerSetupCommand } from "./setup.js";
|
|
25
|
+
// Auto-load .env before Commander parses (so env vars are available for defaults)
|
|
26
|
+
loadDotenv();
|
|
27
|
+
const program = new Command()
|
|
28
|
+
.name("sweny")
|
|
29
|
+
.description("SWEny CLI \u2014 autonomous engineering workflows")
|
|
30
|
+
.version(version);
|
|
31
|
+
// ── sweny init ────────────────────────────────────────────────────────
|
|
32
|
+
program
|
|
33
|
+
.command("init")
|
|
34
|
+
.description("Create a starter .sweny.yml config file")
|
|
35
|
+
.action(() => {
|
|
36
|
+
const target = path.join(process.cwd(), ".sweny.yml");
|
|
37
|
+
if (fs.existsSync(target)) {
|
|
38
|
+
console.error(chalk.yellow(" .sweny.yml already exists — skipping."));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(target, STARTER_CONFIG, "utf-8");
|
|
42
|
+
console.log(chalk.green(" Created .sweny.yml"));
|
|
43
|
+
console.log(chalk.dim(" Add your secrets to .env and run: sweny triage --dry-run"));
|
|
44
|
+
});
|
|
45
|
+
// ── sweny check ───────────────────────────────────────────────────────
|
|
46
|
+
program
|
|
47
|
+
.command("check")
|
|
48
|
+
.description("Verify provider credentials and connectivity")
|
|
49
|
+
.action(async () => {
|
|
50
|
+
const fileConfig = loadConfigFile();
|
|
51
|
+
const config = parseCliInputs({}, fileConfig);
|
|
52
|
+
const errors = validateInputs(config);
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
console.error(formatValidationErrors(errors));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.dim("\n Checking provider connectivity…\n"));
|
|
58
|
+
const results = await checkProviderConnectivity(config);
|
|
59
|
+
console.log(formatCheckResults(results));
|
|
60
|
+
const hasFailure = results.some((r) => r.status === "fail");
|
|
61
|
+
process.exit(hasFailure ? 1 : 0);
|
|
62
|
+
});
|
|
63
|
+
registerSetupCommand(program);
|
|
64
|
+
// ── Credential map builder ──────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Read env vars into the flat credential map expected by buildAutoMcpServers.
|
|
67
|
+
*/
|
|
68
|
+
function buildCredentialMap() {
|
|
69
|
+
const creds = {};
|
|
70
|
+
const env = process.env;
|
|
71
|
+
const keys = [
|
|
72
|
+
"GITHUB_TOKEN",
|
|
73
|
+
"GITLAB_TOKEN",
|
|
74
|
+
"GITLAB_URL",
|
|
75
|
+
"LINEAR_API_KEY",
|
|
76
|
+
"JIRA_URL",
|
|
77
|
+
"JIRA_EMAIL",
|
|
78
|
+
"JIRA_API_TOKEN",
|
|
79
|
+
"DD_API_KEY",
|
|
80
|
+
"DD_APP_KEY",
|
|
81
|
+
"SENTRY_AUTH_TOKEN",
|
|
82
|
+
"SENTRY_ORG",
|
|
83
|
+
"SENTRY_URL",
|
|
84
|
+
"NR_API_KEY",
|
|
85
|
+
"NR_REGION",
|
|
86
|
+
"BETTERSTACK_API_TOKEN",
|
|
87
|
+
"SLACK_BOT_TOKEN",
|
|
88
|
+
"SLACK_TEAM_ID",
|
|
89
|
+
"NOTION_TOKEN",
|
|
90
|
+
"PAGERDUTY_API_TOKEN",
|
|
91
|
+
"MONDAY_TOKEN",
|
|
92
|
+
"ASANA_ACCESS_TOKEN",
|
|
93
|
+
];
|
|
94
|
+
for (const k of keys) {
|
|
95
|
+
if (env[k])
|
|
96
|
+
creds[k] = env[k];
|
|
97
|
+
}
|
|
98
|
+
return creds;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the McpAutoConfig from CLI config for buildAutoMcpServers.
|
|
102
|
+
*/
|
|
103
|
+
function buildMcpAutoConfig(config) {
|
|
104
|
+
return {
|
|
105
|
+
sourceControlProvider: config.sourceControlProvider,
|
|
106
|
+
issueTrackerProvider: config.issueTrackerProvider,
|
|
107
|
+
observabilityProvider: config.observabilityProvider,
|
|
108
|
+
credentials: buildCredentialMap(),
|
|
109
|
+
workspaceTools: config.workspaceTools,
|
|
110
|
+
userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ── sweny triage ──────────────────────────────────────────────────────
|
|
114
|
+
const triageCmd = registerTriageCommand(program);
|
|
115
|
+
triageCmd.action(async (options) => {
|
|
116
|
+
const fileConfig = loadConfigFile();
|
|
117
|
+
const config = parseCliInputs(options, fileConfig);
|
|
118
|
+
// Validate
|
|
119
|
+
const errors = validateInputs(config);
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
console.error(formatValidationErrors(errors));
|
|
122
|
+
console.error(c.subtle("\n Run sweny triage --help for usage information.\n"));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// Non-fatal warnings (e.g. missing service map file)
|
|
126
|
+
for (const warning of validateWarnings(config)) {
|
|
127
|
+
console.warn(chalk.yellow(` \u26A0 ${warning}`));
|
|
128
|
+
}
|
|
129
|
+
// Banner
|
|
130
|
+
if (!config.json) {
|
|
131
|
+
console.log(formatBanner(config, version));
|
|
132
|
+
}
|
|
133
|
+
// ── Build skill map + Claude client ──────────────────────
|
|
134
|
+
const skills = createSkillMap(configuredSkills());
|
|
135
|
+
const claude = new ClaudeClient({
|
|
136
|
+
maxTurns: config.maxInvestigateTurns || 50,
|
|
137
|
+
cwd: process.cwd(),
|
|
138
|
+
logger: consoleLogger,
|
|
139
|
+
});
|
|
140
|
+
// ── Spinner state ──────────────────────────────────────────
|
|
141
|
+
const FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
142
|
+
const isTTY = !config.json && (process.stderr.isTTY ?? false);
|
|
143
|
+
let spinnerInterval;
|
|
144
|
+
let spinnerActive = false;
|
|
145
|
+
let frameIdx = 0;
|
|
146
|
+
let stepStart = 0;
|
|
147
|
+
let stepLabel = "";
|
|
148
|
+
let spinnerStatus = "";
|
|
149
|
+
let currentPhaseColor = chalk.cyan;
|
|
150
|
+
let stepIndex = 0;
|
|
151
|
+
const totalNodes = Object.keys(triageWorkflow.nodes).length;
|
|
152
|
+
function formatElapsed(ms) {
|
|
153
|
+
const s = Math.round(ms / 1000);
|
|
154
|
+
if (s < 60)
|
|
155
|
+
return `${s}s`;
|
|
156
|
+
const m = Math.floor(s / 60);
|
|
157
|
+
return `${m}m ${s % 60}s`;
|
|
158
|
+
}
|
|
159
|
+
function clearSpinnerLine() {
|
|
160
|
+
if (spinnerActive && isTTY) {
|
|
161
|
+
process.stderr.write("\r\x1b[K");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function startSpinner(label) {
|
|
165
|
+
stepStart = Date.now();
|
|
166
|
+
stepLabel = label;
|
|
167
|
+
spinnerStatus = "";
|
|
168
|
+
frameIdx = 0;
|
|
169
|
+
spinnerActive = true;
|
|
170
|
+
if (isTTY) {
|
|
171
|
+
const cols = process.stderr.columns || 80;
|
|
172
|
+
spinnerInterval = setInterval(() => {
|
|
173
|
+
const frame = currentPhaseColor(FRAMES[frameIdx++ % FRAMES.length]);
|
|
174
|
+
const counter = c.subtle(`[${stepIndex}/${totalNodes}]`);
|
|
175
|
+
const elapsed = c.subtle(formatElapsed(Date.now() - stepStart));
|
|
176
|
+
const status = spinnerStatus ? ` ${c.subtle("\u2014")} ${c.subtle(spinnerStatus)}` : "";
|
|
177
|
+
// Truncate to terminal width to prevent line wrapping
|
|
178
|
+
let line = ` ${frame} ${counter} ${stepLabel}${status} ${elapsed}`;
|
|
179
|
+
const visibleLen = line.replace(/\x1B\[[0-9;]*m/g, "").length;
|
|
180
|
+
if (visibleLen > cols) {
|
|
181
|
+
// Re-render without status if too wide
|
|
182
|
+
line = ` ${frame} ${counter} ${stepLabel} ${elapsed}`;
|
|
183
|
+
}
|
|
184
|
+
process.stderr.write(`\r\x1b[K${line}`);
|
|
185
|
+
}, 80);
|
|
186
|
+
}
|
|
187
|
+
else if (!config.json) {
|
|
188
|
+
spinnerInterval = setInterval(() => {
|
|
189
|
+
const elapsed = formatElapsed(Date.now() - stepStart);
|
|
190
|
+
process.stderr.write(` > [${stepIndex}/${totalNodes}] ${stepLabel} ${elapsed}\n`);
|
|
191
|
+
}, 15_000);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function stopSpinner() {
|
|
195
|
+
if (spinnerInterval) {
|
|
196
|
+
clearInterval(spinnerInterval);
|
|
197
|
+
spinnerInterval = undefined;
|
|
198
|
+
}
|
|
199
|
+
if (isTTY) {
|
|
200
|
+
process.stderr.write("\r\x1b[K");
|
|
201
|
+
}
|
|
202
|
+
spinnerActive = false;
|
|
203
|
+
}
|
|
204
|
+
// ── Build observer for DAG events ──────────────────────────
|
|
205
|
+
const runStart = Date.now();
|
|
206
|
+
const observer = config.json
|
|
207
|
+
? undefined
|
|
208
|
+
: (event) => {
|
|
209
|
+
switch (event.type) {
|
|
210
|
+
case "workflow:start":
|
|
211
|
+
// Already printed the banner
|
|
212
|
+
break;
|
|
213
|
+
case "node:enter":
|
|
214
|
+
stepIndex++;
|
|
215
|
+
if (!config.json) {
|
|
216
|
+
startSpinner(event.node);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
case "tool:call":
|
|
220
|
+
if (spinnerActive && isTTY) {
|
|
221
|
+
spinnerStatus = `${event.tool}(...)`;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case "node:exit": {
|
|
225
|
+
stopSpinner();
|
|
226
|
+
if (!config.json) {
|
|
227
|
+
const elapsed = formatElapsed(Date.now() - stepStart);
|
|
228
|
+
const icon = event.result.status === "success"
|
|
229
|
+
? c.ok("\u2713")
|
|
230
|
+
: event.result.status === "skipped"
|
|
231
|
+
? c.subtle("\u2212")
|
|
232
|
+
: c.fail("\u2717");
|
|
233
|
+
const reason = event.result.status !== "success" ? event.result.data?.error : undefined;
|
|
234
|
+
const counter = `[${stepIndex}/${totalNodes}]`;
|
|
235
|
+
console.log(formatStepLine(icon, counter, event.node, elapsed, reason));
|
|
236
|
+
// Inline data details
|
|
237
|
+
const details = getStepDetails(event.node, event.result.data);
|
|
238
|
+
for (const detail of details) {
|
|
239
|
+
console.log(` ${c.subtle("\u21B3")} ${c.subtle(detail)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "route":
|
|
245
|
+
// Optionally log routing decisions
|
|
246
|
+
break;
|
|
247
|
+
case "workflow:end":
|
|
248
|
+
// Output is handled after execute() returns
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
// ── Build workflow input from config ──────────────────────
|
|
253
|
+
// TODO: The triage workflow input structure may need further refinement
|
|
254
|
+
// once the workflow nodes have stabilized. For now, pass config fields
|
|
255
|
+
// that the workflow instructions can reference via the `input` context.
|
|
256
|
+
const workflowInput = {
|
|
257
|
+
timeRange: config.timeRange,
|
|
258
|
+
severityFocus: config.severityFocus,
|
|
259
|
+
serviceFilter: config.serviceFilter,
|
|
260
|
+
investigationDepth: config.investigationDepth,
|
|
261
|
+
repository: config.repository,
|
|
262
|
+
dryRun: config.dryRun,
|
|
263
|
+
baseBranch: config.baseBranch,
|
|
264
|
+
prLabels: config.prLabels,
|
|
265
|
+
issueLabels: config.issueLabels,
|
|
266
|
+
additionalInstructions: config.additionalInstructions,
|
|
267
|
+
issueOverride: config.issueOverride,
|
|
268
|
+
noveltyMode: config.noveltyMode,
|
|
269
|
+
reviewMode: config.reviewMode,
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
const results = await execute(triageWorkflow, workflowInput, {
|
|
273
|
+
skills,
|
|
274
|
+
claude,
|
|
275
|
+
observer,
|
|
276
|
+
logger: consoleLogger,
|
|
277
|
+
});
|
|
278
|
+
const durationMs = Date.now() - runStart;
|
|
279
|
+
// Output
|
|
280
|
+
if (config.json) {
|
|
281
|
+
console.log(formatResultJson(results));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
console.log(formatDagResultHuman(results, durationMs, config));
|
|
285
|
+
}
|
|
286
|
+
// Terminal bell
|
|
287
|
+
if (config.bell)
|
|
288
|
+
process.stderr.write("\x07");
|
|
289
|
+
// Check if any node failed
|
|
290
|
+
const hasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
291
|
+
process.exit(hasFailed ? 1 : 0);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (config.json) {
|
|
295
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
console.error(formatCrashError(error));
|
|
299
|
+
}
|
|
300
|
+
// Terminal bell even on crash
|
|
301
|
+
if (config.bell)
|
|
302
|
+
process.stderr.write("\x07");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
// ── sweny implement ───────────────────────────────────────────────────
|
|
307
|
+
const implementCmd = registerImplementCommand(program);
|
|
308
|
+
implementCmd.action(async (issueId, options) => {
|
|
309
|
+
const fileConfig = loadConfigFile();
|
|
310
|
+
// Build a minimal CliConfig for the implement command by merging CLI opts with env/file
|
|
311
|
+
const config = {
|
|
312
|
+
...parseCliInputs(options, fileConfig),
|
|
313
|
+
// Override specific fields that differ for implement
|
|
314
|
+
issueTrackerProvider: options.issueTrackerProvider || fileConfig["issue-tracker-provider"] || "linear",
|
|
315
|
+
sourceControlProvider: options.sourceControlProvider || fileConfig["source-control-provider"] || "github",
|
|
316
|
+
codingAgentProvider: options.codingAgentProvider || fileConfig["coding-agent-provider"] || "claude",
|
|
317
|
+
dryRun: Boolean(options.dryRun),
|
|
318
|
+
maxImplementTurns: parseInt(String(options.maxImplementTurns || fileConfig["max-implement-turns"] || "40"), 10),
|
|
319
|
+
baseBranch: options.baseBranch || fileConfig["base-branch"] || "main",
|
|
320
|
+
repository: options.repository || process.env.GITHUB_REPOSITORY || "",
|
|
321
|
+
outputDir: options.outputDir || process.env.SWENY_OUTPUT_DIR || fileConfig["output-dir"] || ".sweny/output",
|
|
322
|
+
};
|
|
323
|
+
const skills = createSkillMap(configuredSkills());
|
|
324
|
+
const claude = new ClaudeClient({
|
|
325
|
+
maxTurns: config.maxImplementTurns || 40,
|
|
326
|
+
cwd: process.cwd(),
|
|
327
|
+
logger: consoleLogger,
|
|
328
|
+
});
|
|
329
|
+
console.log(chalk.cyan(`\n sweny implement ${issueId}\n`));
|
|
330
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
331
|
+
const observer = (event) => {
|
|
332
|
+
switch (event.type) {
|
|
333
|
+
case "workflow:start":
|
|
334
|
+
process.stderr.write(`\n \u25B2 ${chalk.bold(event.workflow)}\n\n`);
|
|
335
|
+
break;
|
|
336
|
+
case "node:enter":
|
|
337
|
+
process.stderr.write(` ${c.subtle("\u25CB")} ${chalk.dim(event.node)}\u2026\n`);
|
|
338
|
+
break;
|
|
339
|
+
case "node:exit": {
|
|
340
|
+
const icon = event.result.status === "success"
|
|
341
|
+
? c.ok("\u2713")
|
|
342
|
+
: event.result.status === "skipped"
|
|
343
|
+
? c.subtle("\u2212")
|
|
344
|
+
: c.fail("\u2717");
|
|
345
|
+
if (isTTY) {
|
|
346
|
+
process.stderr.write(`\x1B[1A\x1B[2K ${icon} ${event.node}\n`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
process.stderr.write(` ${icon} ${event.node}\n`);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "workflow:end":
|
|
354
|
+
process.stderr.write(`\n`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
// Build workflow input for implement
|
|
359
|
+
const workflowInput = {
|
|
360
|
+
issueIdentifier: issueId,
|
|
361
|
+
repository: config.repository,
|
|
362
|
+
dryRun: config.dryRun,
|
|
363
|
+
baseBranch: config.baseBranch,
|
|
364
|
+
prLabels: config.prLabels,
|
|
365
|
+
reviewMode: config.reviewMode,
|
|
366
|
+
additionalInstructions: config.additionalInstructions,
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
const results = await execute(implementWorkflow, workflowInput, {
|
|
370
|
+
skills,
|
|
371
|
+
claude,
|
|
372
|
+
observer,
|
|
373
|
+
logger: consoleLogger,
|
|
374
|
+
});
|
|
375
|
+
const hasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
376
|
+
if (hasFailed) {
|
|
377
|
+
console.error(chalk.red(`\n Implement workflow failed\n`));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
const prResult = results.get("create_pr");
|
|
381
|
+
const prUrl = prResult?.data?.prUrl;
|
|
382
|
+
if (prUrl) {
|
|
383
|
+
console.log(chalk.green(`\n PR created: ${prUrl}\n`));
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
console.log(chalk.green(`\n Implement workflow completed\n`));
|
|
387
|
+
}
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
console.error(chalk.red(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// ── sweny workflow ─────────────────────────────────────────────────────
|
|
396
|
+
function promptUser(question) {
|
|
397
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
398
|
+
return new Promise((resolve) => {
|
|
399
|
+
rl.question(question, (answer) => {
|
|
400
|
+
rl.close();
|
|
401
|
+
resolve(answer.trim());
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const workflowCmd = program.command("workflow").description("Manage and run workflow files");
|
|
406
|
+
/** Reads and parses a workflow file (YAML or JSON). Throws on I/O or parse error. */
|
|
407
|
+
function parseWorkflowFileContent(filePath) {
|
|
408
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
409
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
410
|
+
return ext === ".yaml" || ext === ".yml" ? parseYaml(content) : JSON.parse(content);
|
|
411
|
+
}
|
|
412
|
+
export function loadWorkflowFile(filePath) {
|
|
413
|
+
const raw = parseWorkflowFileContent(filePath);
|
|
414
|
+
const errors = validateWorkflowSchema(raw);
|
|
415
|
+
if (errors.length > 0) {
|
|
416
|
+
throw new Error(`Invalid workflow file:\n${errors.map((e) => ` ${e.message}`).join("\n")}`);
|
|
417
|
+
}
|
|
418
|
+
return raw;
|
|
419
|
+
}
|
|
420
|
+
export async function workflowRunAction(file, options) {
|
|
421
|
+
let workflow;
|
|
422
|
+
try {
|
|
423
|
+
workflow = loadWorkflowFile(file);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
console.error(chalk.red(` Error loading workflow file: ${err instanceof Error ? err.message : String(err)}`));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (options.dryRun) {
|
|
431
|
+
console.log(chalk.green(` Workflow "${workflow.name}" is valid (${Object.keys(workflow.nodes).length} nodes)`));
|
|
432
|
+
for (const [id, node] of Object.entries(workflow.nodes)) {
|
|
433
|
+
console.log(chalk.dim(` ${id}: ${node.name}${node.skills.length ? ` skills=[${node.skills.join(",")}]` : ""}`));
|
|
434
|
+
}
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
const fileConfig = loadConfigFile();
|
|
438
|
+
const config = parseCliInputs(options, fileConfig);
|
|
439
|
+
const isJson = Boolean(options.json);
|
|
440
|
+
const isTTY = !isJson && (process.stderr.isTTY ?? false);
|
|
441
|
+
const skills = createSkillMap(configuredSkills());
|
|
442
|
+
const claude = new ClaudeClient({
|
|
443
|
+
maxTurns: config.maxInvestigateTurns || 50,
|
|
444
|
+
cwd: process.cwd(),
|
|
445
|
+
logger: consoleLogger,
|
|
446
|
+
});
|
|
447
|
+
// Track per-node entry time to compute elapsed on exit
|
|
448
|
+
const nodeEnterTimes = new Map();
|
|
449
|
+
const observer = isJson
|
|
450
|
+
? undefined
|
|
451
|
+
: (event) => {
|
|
452
|
+
switch (event.type) {
|
|
453
|
+
case "workflow:start":
|
|
454
|
+
process.stderr.write(`\n \u25B2 ${chalk.bold(event.workflow)}\n\n`);
|
|
455
|
+
break;
|
|
456
|
+
case "node:enter":
|
|
457
|
+
nodeEnterTimes.set(event.node, Date.now());
|
|
458
|
+
process.stderr.write(` ${c.subtle("\u25CB")} ${chalk.dim(event.node)}\u2026\n`);
|
|
459
|
+
break;
|
|
460
|
+
case "node:exit": {
|
|
461
|
+
const icon = event.result.status === "success"
|
|
462
|
+
? c.ok("\u2713")
|
|
463
|
+
: event.result.status === "skipped"
|
|
464
|
+
? c.subtle("\u2212")
|
|
465
|
+
: c.fail("\u2717");
|
|
466
|
+
const enterTime = nodeEnterTimes.get(event.node) ?? Date.now();
|
|
467
|
+
const elapsedMs = Date.now() - enterTime;
|
|
468
|
+
const elapsed = chalk.dim(elapsedMs < 1000 ? `${elapsedMs}ms` : `${Math.round(elapsedMs / 100) / 10}s`);
|
|
469
|
+
if (isTTY) {
|
|
470
|
+
// Overwrite the pending "○ nodeId…" line with the final status
|
|
471
|
+
process.stderr.write(`\x1B[1A\x1B[2K ${icon} ${event.node} ${elapsed}\n`);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
process.stderr.write(` ${icon} ${event.node} ${elapsed}\n`);
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "workflow:end":
|
|
479
|
+
process.stderr.write(`\n`);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
// Build workflow input from config
|
|
484
|
+
const workflowInput = {
|
|
485
|
+
timeRange: config.timeRange,
|
|
486
|
+
severityFocus: config.severityFocus,
|
|
487
|
+
serviceFilter: config.serviceFilter,
|
|
488
|
+
repository: config.repository,
|
|
489
|
+
dryRun: config.dryRun,
|
|
490
|
+
baseBranch: config.baseBranch,
|
|
491
|
+
prLabels: config.prLabels,
|
|
492
|
+
additionalInstructions: config.additionalInstructions,
|
|
493
|
+
};
|
|
494
|
+
try {
|
|
495
|
+
const results = await execute(workflow, workflowInput, {
|
|
496
|
+
skills,
|
|
497
|
+
claude,
|
|
498
|
+
observer,
|
|
499
|
+
logger: consoleLogger,
|
|
500
|
+
});
|
|
501
|
+
if (isJson) {
|
|
502
|
+
process.stdout.write(JSON.stringify(Object.fromEntries(results), null, 2) + "\n");
|
|
503
|
+
const hasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
504
|
+
process.exit(hasFailed ? 1 : 0);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const hasFailed = [...results.values()].some((r) => r.status === "failed");
|
|
508
|
+
if (hasFailed) {
|
|
509
|
+
console.error(chalk.red(` Workflow failed\n`));
|
|
510
|
+
process.exit(1);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
console.log(chalk.green(` Workflow completed\n`));
|
|
514
|
+
process.exit(0);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
console.error(chalk.red(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
export function workflowExportAction(name) {
|
|
522
|
+
let workflow;
|
|
523
|
+
if (name === "triage") {
|
|
524
|
+
workflow = triageWorkflow;
|
|
525
|
+
}
|
|
526
|
+
else if (name === "implement") {
|
|
527
|
+
workflow = implementWorkflow;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.error(chalk.red(` Unknown workflow "${name}". Available: triage, implement`));
|
|
531
|
+
process.exit(1);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// Export as YAML
|
|
535
|
+
process.stdout.write(stringifyYaml(workflow, { indent: 2, lineWidth: 120 }));
|
|
536
|
+
}
|
|
537
|
+
export function workflowValidateAction(file, options) {
|
|
538
|
+
let raw;
|
|
539
|
+
try {
|
|
540
|
+
raw = parseWorkflowFileContent(file);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
544
|
+
if (options.json) {
|
|
545
|
+
process.stderr.write(JSON.stringify({ valid: false, errors: [{ message }] }, null, 2) + "\n");
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
console.error(chalk.red(` Cannot read "${file}": ${message}`));
|
|
549
|
+
}
|
|
550
|
+
process.exit(1);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
554
|
+
const kind = raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw;
|
|
555
|
+
const message = `Expected a YAML/JSON object, got ${kind}`;
|
|
556
|
+
if (options.json) {
|
|
557
|
+
process.stderr.write(JSON.stringify({ valid: false, errors: [{ message }] }, null, 2) + "\n");
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
console.error(chalk.red(` \u2717 ${file}: ${message}`));
|
|
561
|
+
}
|
|
562
|
+
process.exit(1);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const errors = validateWorkflowSchema(raw);
|
|
566
|
+
if (options.json) {
|
|
567
|
+
process.stdout.write(JSON.stringify({ valid: errors.length === 0, errors }, null, 2) + "\n");
|
|
568
|
+
}
|
|
569
|
+
else if (errors.length === 0) {
|
|
570
|
+
console.log(chalk.green(` \u2713 ${file} is valid`));
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
console.error(chalk.red(` \u2717 ${file} has ${errors.length} validation error${errors.length > 1 ? "s" : ""}:`));
|
|
574
|
+
for (const err of errors) {
|
|
575
|
+
console.error(chalk.dim(` ${err.message}`));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
process.exit(errors.length === 0 ? 0 : 1);
|
|
579
|
+
}
|
|
580
|
+
workflowCmd
|
|
581
|
+
.command("validate <file>")
|
|
582
|
+
.description("Validate a workflow YAML or JSON file")
|
|
583
|
+
.option("--json", "Output result as JSON")
|
|
584
|
+
.action(workflowValidateAction);
|
|
585
|
+
workflowCmd
|
|
586
|
+
.command("run <file>")
|
|
587
|
+
.description("Run a workflow from a YAML or JSON file")
|
|
588
|
+
.option("--dry-run", "Validate workflow without running")
|
|
589
|
+
.option("--json", "Output result as JSON on stdout; suppress progress output")
|
|
590
|
+
.action(workflowRunAction);
|
|
591
|
+
workflowCmd
|
|
592
|
+
.command("export <name>")
|
|
593
|
+
.description("Print a built-in workflow as YAML (triage or implement)")
|
|
594
|
+
.action(workflowExportAction);
|
|
595
|
+
workflowCmd
|
|
596
|
+
.command("create <description>")
|
|
597
|
+
.description("Generate a new workflow from a natural language description")
|
|
598
|
+
.option("--json", "Output workflow JSON to stdout (no interactive prompt)")
|
|
599
|
+
.action(async (description, options) => {
|
|
600
|
+
const skills = configuredSkills();
|
|
601
|
+
const claude = new ClaudeClient({
|
|
602
|
+
maxTurns: 3,
|
|
603
|
+
cwd: process.cwd(),
|
|
604
|
+
logger: consoleLogger,
|
|
605
|
+
});
|
|
606
|
+
try {
|
|
607
|
+
let workflow = await buildWorkflow(description, { claude, skills, logger: consoleLogger });
|
|
608
|
+
if (options.json) {
|
|
609
|
+
process.stdout.write(JSON.stringify(workflow, null, 2) + "\n");
|
|
610
|
+
process.exit(0);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
while (true) {
|
|
614
|
+
console.log("");
|
|
615
|
+
const renderer = new DagRenderer(workflow, { animate: false });
|
|
616
|
+
console.log(renderer.renderToString());
|
|
617
|
+
console.log("");
|
|
618
|
+
const defaultPath = `.sweny/workflows/${workflow.id}.yml`;
|
|
619
|
+
const answer = await promptUser(` Save to ${defaultPath}? [Y/n/refine] `);
|
|
620
|
+
const choice = answer.toLowerCase() || "y";
|
|
621
|
+
if (choice === "y" || choice === "yes") {
|
|
622
|
+
const dir = path.dirname(defaultPath);
|
|
623
|
+
if (!fs.existsSync(dir))
|
|
624
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
625
|
+
fs.writeFileSync(defaultPath, stringifyYaml(workflow, { indent: 2, lineWidth: 120 }), "utf-8");
|
|
626
|
+
console.log(chalk.green(`\n Saved to ${defaultPath}\n`));
|
|
627
|
+
process.exit(0);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
else if (choice === "n" || choice === "no") {
|
|
631
|
+
console.log(chalk.dim("\n Discarded.\n"));
|
|
632
|
+
process.exit(0);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
else if (choice === "refine" || choice === "r") {
|
|
636
|
+
const refinement = await promptUser(" What would you like to change? ");
|
|
637
|
+
if (!refinement)
|
|
638
|
+
continue;
|
|
639
|
+
console.log(chalk.dim("\n Refining...\n"));
|
|
640
|
+
workflow = await refineWorkflow(workflow, refinement, { claude, skills, logger: consoleLogger });
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
console.log(chalk.dim("\n Refining...\n"));
|
|
644
|
+
workflow = await refineWorkflow(workflow, choice, { claude, skills, logger: consoleLogger });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
console.error(chalk.red(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
workflowCmd
|
|
654
|
+
.command("edit <file> [instruction]")
|
|
655
|
+
.description("Edit an existing workflow file with natural language instructions")
|
|
656
|
+
.option("--json", "Output updated workflow JSON to stdout (no interactive prompt)")
|
|
657
|
+
.action(async (file, instruction, options) => {
|
|
658
|
+
let workflow;
|
|
659
|
+
try {
|
|
660
|
+
workflow = loadWorkflowFile(file);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
console.error(chalk.red(` Error loading ${file}: ${err instanceof Error ? err.message : String(err)}`));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const skills = configuredSkills();
|
|
668
|
+
const claude = new ClaudeClient({
|
|
669
|
+
maxTurns: 3,
|
|
670
|
+
cwd: process.cwd(),
|
|
671
|
+
logger: consoleLogger,
|
|
672
|
+
});
|
|
673
|
+
if (!instruction) {
|
|
674
|
+
instruction = await promptUser(" What would you like to change? ");
|
|
675
|
+
if (!instruction) {
|
|
676
|
+
console.log(chalk.dim(" No changes.\n"));
|
|
677
|
+
process.exit(0);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
let updated = await refineWorkflow(workflow, instruction, { claude, skills, logger: consoleLogger });
|
|
683
|
+
if (options.json) {
|
|
684
|
+
process.stdout.write(JSON.stringify(updated, null, 2) + "\n");
|
|
685
|
+
process.exit(0);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
while (true) {
|
|
689
|
+
console.log("");
|
|
690
|
+
const renderer = new DagRenderer(updated, { animate: false });
|
|
691
|
+
console.log(renderer.renderToString());
|
|
692
|
+
console.log("");
|
|
693
|
+
const answer = await promptUser(` Save changes to ${file}? [Y/n/refine] `);
|
|
694
|
+
const choice = answer.toLowerCase() || "y";
|
|
695
|
+
if (choice === "y" || choice === "yes") {
|
|
696
|
+
fs.writeFileSync(file, stringifyYaml(updated, { indent: 2, lineWidth: 120 }), "utf-8");
|
|
697
|
+
console.log(chalk.green(`\n Saved to ${file}\n`));
|
|
698
|
+
process.exit(0);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
else if (choice === "n" || choice === "no") {
|
|
702
|
+
console.log(chalk.dim("\n Discarded.\n"));
|
|
703
|
+
process.exit(0);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
else if (choice === "refine" || choice === "r") {
|
|
707
|
+
const refinement = await promptUser(" What would you like to change? ");
|
|
708
|
+
if (!refinement)
|
|
709
|
+
continue;
|
|
710
|
+
console.log(chalk.dim("\n Refining...\n"));
|
|
711
|
+
updated = await refineWorkflow(updated, refinement, { claude, skills, logger: consoleLogger });
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
console.log(chalk.dim("\n Refining...\n"));
|
|
715
|
+
updated = await refineWorkflow(updated, choice, { claude, skills, logger: consoleLogger });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
console.error(chalk.red(`\n Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// TODO: The old CLI had `workflow list` that showed registered step types
|
|
725
|
+
// from the engine. In the new DAG model, we list available skills instead.
|
|
726
|
+
workflowCmd
|
|
727
|
+
.command("list")
|
|
728
|
+
.description("List available skills")
|
|
729
|
+
.option("--json", "Output as JSON array")
|
|
730
|
+
.action((options) => {
|
|
731
|
+
const skills = configuredSkills();
|
|
732
|
+
if (options.json) {
|
|
733
|
+
const data = skills.map((s) => ({ id: s.id, name: s.name, description: s.description, category: s.category }));
|
|
734
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
console.log(chalk.bold("\nConfigured skills:\n"));
|
|
738
|
+
for (const skill of skills) {
|
|
739
|
+
console.log(` ${chalk.cyan(skill.id)} (${skill.category})`);
|
|
740
|
+
console.log(chalk.dim(` ${skill.description}`));
|
|
741
|
+
}
|
|
742
|
+
console.log();
|
|
743
|
+
});
|
|
744
|
+
program.parse();
|