@sweny-ai/core 0.1.9 → 0.1.11
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/cli/output.js +62 -5
- package/dist/executor.js +21 -4
- package/dist/workflows/triage.js +67 -59
- package/package.json +1 -1
package/dist/cli/output.js
CHANGED
|
@@ -201,6 +201,15 @@ export function formatDagResultHuman(results, durationMs, config) {
|
|
|
201
201
|
if (createPrResult?.data?.prUrl) {
|
|
202
202
|
return formatDagSuccessResult(results, duration);
|
|
203
203
|
}
|
|
204
|
+
// Dry run — show findings summary, no side effects taken
|
|
205
|
+
if (config?.dryRun) {
|
|
206
|
+
return formatDagDryRunResult(results, duration);
|
|
207
|
+
}
|
|
208
|
+
// Issues created but no PR (fix too complex)
|
|
209
|
+
const createIssueResult = results.get("create_issue");
|
|
210
|
+
if (createIssueResult && createIssueResult.status === "success") {
|
|
211
|
+
return formatDagIssuesCreatedResult(results, duration);
|
|
212
|
+
}
|
|
204
213
|
// No action / skip
|
|
205
214
|
return formatDagNoActionResult(results, duration, config);
|
|
206
215
|
}
|
|
@@ -226,6 +235,56 @@ function formatDagSuccessResult(results, duration) {
|
|
|
226
235
|
}
|
|
227
236
|
return ["", boxTop(), ...boxSection(header), boxDivider(), ...boxSection(body), boxBottom(), ""].join("\n");
|
|
228
237
|
}
|
|
238
|
+
function formatDagIssuesCreatedResult(results, duration) {
|
|
239
|
+
const title = `${c.ok("\u2713")} ${chalk.bold("Issues Created")}`;
|
|
240
|
+
const titlePad = BOX_WIDTH - 4 - visLen(title) - visLen(duration);
|
|
241
|
+
const header = [title + " ".repeat(Math.max(1, titlePad)) + c.subtle(duration)];
|
|
242
|
+
const body = [];
|
|
243
|
+
const issueData = results.get("create_issue")?.data;
|
|
244
|
+
if (issueData?.issueIdentifier) {
|
|
245
|
+
body.push(`${c.subtle("Issue")}${" ".repeat(5)}${chalk.bold(String(issueData.issueIdentifier))}`);
|
|
246
|
+
if (issueData.issueTitle)
|
|
247
|
+
body.push(`${" ".repeat(10)}${String(issueData.issueTitle)}`);
|
|
248
|
+
if (issueData.issueUrl)
|
|
249
|
+
body.push(`${" ".repeat(10)}${c.link(String(issueData.issueUrl))}`);
|
|
250
|
+
body.push("");
|
|
251
|
+
}
|
|
252
|
+
const investigateData = results.get("investigate")?.data;
|
|
253
|
+
const rec = investigateData?.recommendation;
|
|
254
|
+
if (rec) {
|
|
255
|
+
body.push(`${c.subtle("Next")}${" ".repeat(6)}${String(rec)}`);
|
|
256
|
+
}
|
|
257
|
+
return ["", boxTop(), ...boxSection(header), boxDivider(), ...boxSection(body), boxBottom(), ""].join("\n");
|
|
258
|
+
}
|
|
259
|
+
function formatDagDryRunResult(results, duration) {
|
|
260
|
+
const title = `${c.ok("\u2713")} ${chalk.bold("Triage Complete (Dry Run)")}`;
|
|
261
|
+
const titlePad = BOX_WIDTH - 4 - visLen(title) - visLen(duration);
|
|
262
|
+
const header = [title + " ".repeat(Math.max(1, titlePad)) + c.subtle(duration)];
|
|
263
|
+
const body = [];
|
|
264
|
+
const investigateData = results.get("investigate")?.data;
|
|
265
|
+
const findings = investigateData?.findings;
|
|
266
|
+
const novelCount = investigateData?.novel_count;
|
|
267
|
+
const severity = investigateData?.highest_severity;
|
|
268
|
+
if (findings && findings.length > 0) {
|
|
269
|
+
body.push(`${c.subtle("Findings")}${" ".repeat(2)}${chalk.bold(String(findings.length))} total, ${chalk.bold(String(novelCount ?? 0))} novel`);
|
|
270
|
+
if (severity)
|
|
271
|
+
body.push(`${c.subtle("Severity")}${" ".repeat(2)}${chalk.bold(severity)}`);
|
|
272
|
+
body.push("");
|
|
273
|
+
for (const f of findings.slice(0, 5)) {
|
|
274
|
+
const dup = f.is_duplicate ? c.subtle(" (dup)") : "";
|
|
275
|
+
body.push(` ${f.severity === "critical" || f.severity === "high" ? c.fail("\u25CF") : c.subtle("\u25CB")} ${String(f.title)}${dup}`);
|
|
276
|
+
}
|
|
277
|
+
if (findings.length > 5)
|
|
278
|
+
body.push(c.subtle(` ... and ${findings.length - 5} more`));
|
|
279
|
+
body.push("");
|
|
280
|
+
}
|
|
281
|
+
const rec = investigateData?.recommendation;
|
|
282
|
+
if (rec)
|
|
283
|
+
body.push(`${c.subtle("Next")}${" ".repeat(6)}${String(rec)}`);
|
|
284
|
+
body.push("");
|
|
285
|
+
body.push(c.subtle("No side effects — dry run mode"));
|
|
286
|
+
return ["", boxTop(), ...boxSection(header), boxDivider(), ...boxSection(body), boxBottom(), ""].join("\n");
|
|
287
|
+
}
|
|
229
288
|
function formatDagFailureResult(nodeId, result, duration) {
|
|
230
289
|
const title = `${c.fail("\u2717")} ${chalk.bold("Workflow Failed")}`;
|
|
231
290
|
const titlePad = BOX_WIDTH - 4 - visLen(title) - visLen(duration);
|
|
@@ -244,11 +303,9 @@ function formatDagNoActionResult(results, duration, config) {
|
|
|
244
303
|
const header = [title + " ".repeat(Math.max(1, titlePad)) + c.subtle(duration)];
|
|
245
304
|
const body = [];
|
|
246
305
|
const investigateData = results.get("investigate")?.data;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
body.push(`${" ".repeat(2)}${c.link(String(investigateData.duplicate_of))}`);
|
|
251
|
-
}
|
|
306
|
+
const novelCount = investigateData?.novel_count;
|
|
307
|
+
if (novelCount === 0) {
|
|
308
|
+
body.push("All findings were duplicates of existing issues.");
|
|
252
309
|
}
|
|
253
310
|
else {
|
|
254
311
|
const rec = investigateData?.recommendation;
|
package/dist/executor.js
CHANGED
|
@@ -61,8 +61,21 @@ export async function execute(workflow, input, options) {
|
|
|
61
61
|
results.set(currentId, result);
|
|
62
62
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
63
63
|
logger.info(` ✓ ${result.status}`, { node: currentId, toolCalls: result.toolCalls.length });
|
|
64
|
+
// Dry run hard gate — stop at the first conditional routing decision.
|
|
65
|
+
// Unconditional edges are analysis flow (prepare→gather→investigate);
|
|
66
|
+
// conditional edges are action decisions (investigate→create_issue/skip).
|
|
67
|
+
// Enforced in the executor so it cannot be bypassed by LLM evaluation.
|
|
68
|
+
const isDryRun = input && typeof input === "object" && input.dryRun === true;
|
|
69
|
+
if (isDryRun) {
|
|
70
|
+
const outEdges = workflow.edges.filter((e) => e.from === currentId);
|
|
71
|
+
if (outEdges.some((e) => e.when)) {
|
|
72
|
+
safeObserve(observer, { type: "route", from: currentId, to: "(end)", reason: "dry run" }, logger);
|
|
73
|
+
currentId = null;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
// Resolve next node via edge conditions
|
|
65
|
-
currentId = await resolveNext(workflow, currentId, results, claude, observer);
|
|
78
|
+
currentId = await resolveNext(workflow, currentId, results, input, claude, observer);
|
|
66
79
|
}
|
|
67
80
|
safeObserve(observer, {
|
|
68
81
|
type: "workflow:end",
|
|
@@ -148,7 +161,7 @@ function resolveConfig(skills, overrides) {
|
|
|
148
161
|
* - 1 unconditional edge → follow it
|
|
149
162
|
* - Multiple or conditional → Claude evaluates
|
|
150
163
|
*/
|
|
151
|
-
async function resolveNext(workflow, current, results, claude, observer) {
|
|
164
|
+
async function resolveNext(workflow, current, results, input, claude, observer) {
|
|
152
165
|
const outEdges = workflow.edges.filter((e) => e.from === current);
|
|
153
166
|
if (outEdges.length === 0)
|
|
154
167
|
return null;
|
|
@@ -160,8 +173,12 @@ async function resolveNext(workflow, current, results, claude, observer) {
|
|
|
160
173
|
// Check for a default (unconditional) edge among conditionals
|
|
161
174
|
const defaultEdge = outEdges.find((e) => !e.when);
|
|
162
175
|
const conditionalEdges = outEdges.filter((e) => e.when);
|
|
163
|
-
// Claude evaluates which condition matches
|
|
164
|
-
|
|
176
|
+
// Claude evaluates which condition matches — include input so conditions
|
|
177
|
+
// can reference workflow-level flags like dryRun
|
|
178
|
+
const context = {
|
|
179
|
+
input,
|
|
180
|
+
...Object.fromEntries([...results.entries()].map(([k, v]) => [k, v.data])),
|
|
181
|
+
};
|
|
165
182
|
const choices = conditionalEdges.map((e) => ({
|
|
166
183
|
id: e.to,
|
|
167
184
|
description: e.when,
|
package/dist/workflows/triage.js
CHANGED
|
@@ -47,66 +47,84 @@ Be thorough — the investigation step depends on complete context. Use every to
|
|
|
47
47
|
},
|
|
48
48
|
investigate: {
|
|
49
49
|
name: "Root Cause Analysis",
|
|
50
|
-
instruction: `Based on the gathered context,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
50
|
+
instruction: `Based on the gathered context, classify every distinct issue you found into one of two buckets: **novel** or **duplicate**.
|
|
51
|
+
|
|
52
|
+
For EACH issue found:
|
|
53
|
+
1. Identify the root cause and affected code/service.
|
|
54
|
+
2. Assess severity: critical (service down), high (major feature broken), medium (degraded), low (cosmetic/minor).
|
|
55
|
+
3. Assess fix complexity: "simple" (a few lines, clear change), "moderate" (multiple files but well-understood), or "complex" (architectural, risky, or unclear).
|
|
56
|
+
4. **Novelty check (REQUIRED):** Search the issue tracker for existing issues (BOTH open AND closed) that cover the same root cause, error pattern, or affected service. Use github_search_issues and/or linear_search_issues with multiple keyword variations.
|
|
57
|
+
- A match = same root cause, same error message/pattern, or a human would call it "the same bug."
|
|
58
|
+
- If matched → it's a **duplicate**. Record the existing issue ID.
|
|
59
|
+
- If no match → it's **novel**.
|
|
60
|
+
|
|
61
|
+
**Output rules:**
|
|
62
|
+
- \`findings\`: array of ALL issues found (both novel and duplicate).
|
|
63
|
+
- \`novel_count\`: how many findings are novel (not duplicates).
|
|
64
|
+
- \`highest_severity\`: the highest severity across ALL findings.
|
|
65
|
+
- \`recommendation\`: what should happen next.
|
|
66
|
+
|
|
67
|
+
Downstream nodes will act ONLY on novel findings. Duplicates will be +1'd automatically.`,
|
|
68
68
|
skills: ["github", "linear"],
|
|
69
69
|
output: {
|
|
70
70
|
type: "object",
|
|
71
71
|
properties: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
findings: {
|
|
73
|
+
type: "array",
|
|
74
|
+
items: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
title: { type: "string", description: "Short description of the issue" },
|
|
78
|
+
root_cause: { type: "string" },
|
|
79
|
+
severity: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
80
|
+
affected_services: { type: "array", items: { type: "string" } },
|
|
81
|
+
is_duplicate: { type: "boolean" },
|
|
82
|
+
duplicate_of: { type: "string", description: "Existing issue ID/URL if duplicate" },
|
|
83
|
+
fix_approach: { type: "string" },
|
|
84
|
+
fix_complexity: { type: "string", enum: ["simple", "moderate", "complex"] },
|
|
85
|
+
},
|
|
86
|
+
required: ["title", "root_cause", "severity", "is_duplicate"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
novel_count: { type: "number", description: "Count of novel (non-duplicate) findings" },
|
|
90
|
+
highest_severity: { type: "string", enum: ["critical", "high", "medium", "low"] },
|
|
77
91
|
recommendation: { type: "string" },
|
|
78
|
-
fix_approach: { type: "string" },
|
|
79
|
-
fix_complexity: { type: "string", enum: ["simple", "moderate", "complex"] },
|
|
80
92
|
},
|
|
81
|
-
required: ["
|
|
93
|
+
required: ["findings", "novel_count", "highest_severity", "recommendation"],
|
|
82
94
|
},
|
|
83
95
|
},
|
|
84
96
|
create_issue: {
|
|
85
|
-
name: "Create
|
|
86
|
-
instruction: `
|
|
97
|
+
name: "Create Issues & Triage Duplicates",
|
|
98
|
+
instruction: `Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
**For each NOVEL finding** (is_duplicate = false):
|
|
101
|
+
1. Create a new issue with a clear, actionable title.
|
|
89
102
|
2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.
|
|
90
103
|
3. Add appropriate labels (bug, severity level, affected service).
|
|
91
104
|
4. Link to relevant commits, PRs, or existing issues.
|
|
92
105
|
|
|
93
|
-
**
|
|
106
|
+
**For each DUPLICATE finding** (is_duplicate = true):
|
|
107
|
+
1. Find the existing issue (check duplicate_of field).
|
|
108
|
+
2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last 24 hours, skip adding another comment.
|
|
109
|
+
3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)
|
|
110
|
+
4. If the existing issue is closed/done, reopen it.
|
|
94
111
|
|
|
95
|
-
If context.issueTemplate is provided, use it as the format for
|
|
112
|
+
If context.issueTemplate is provided, use it as the format for new issue bodies. Otherwise use a clear structure with: Summary, Root Cause, Impact, Steps to Reproduce, and Recommended Fix.
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
Use whichever issue tracker is available to you. Output the created/updated issue identifiers.`,
|
|
98
115
|
skills: ["linear", "github"],
|
|
99
116
|
},
|
|
100
117
|
skip: {
|
|
101
|
-
name: "Skip —
|
|
102
|
-
instruction: `
|
|
118
|
+
name: "Skip — All Duplicates or Low Priority",
|
|
119
|
+
instruction: `Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
1. Find the existing issue
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
121
|
+
For each **duplicate** finding (check the findings array for items where is_duplicate = true):
|
|
122
|
+
1. Find the existing issue (check duplicate_of field).
|
|
123
|
+
2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last 24 hours, skip adding another comment.
|
|
124
|
+
3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)
|
|
125
|
+
4. If the issue is closed/done, reopen it.
|
|
108
126
|
|
|
109
|
-
|
|
127
|
+
For **low priority** findings, log a brief note about why they were skipped.`,
|
|
110
128
|
skills: ["linear", "github"],
|
|
111
129
|
},
|
|
112
130
|
implement: {
|
|
@@ -155,42 +173,32 @@ Use whichever notification channel is available to you.`,
|
|
|
155
173
|
{ from: "prepare", to: "gather" },
|
|
156
174
|
// gather → investigate (always)
|
|
157
175
|
{ from: "gather", to: "investigate" },
|
|
158
|
-
// investigate → create_issue (
|
|
176
|
+
// investigate → create_issue (novel findings worth acting on)
|
|
159
177
|
{
|
|
160
178
|
from: "investigate",
|
|
161
179
|
to: "create_issue",
|
|
162
|
-
when: "
|
|
180
|
+
when: "novel_count is greater than 0 AND highest_severity is medium or higher",
|
|
163
181
|
},
|
|
164
|
-
// investigate → skip (
|
|
182
|
+
// investigate → skip (everything is a duplicate or low priority)
|
|
165
183
|
{
|
|
166
184
|
from: "investigate",
|
|
167
185
|
to: "skip",
|
|
168
|
-
when: "
|
|
186
|
+
when: "novel_count is 0, OR highest_severity is low",
|
|
169
187
|
},
|
|
170
|
-
// create_issue → implement (
|
|
188
|
+
// create_issue → implement (novel findings have a clear, feasible fix)
|
|
171
189
|
{
|
|
172
190
|
from: "create_issue",
|
|
173
191
|
to: "implement",
|
|
174
|
-
when: "fix_complexity
|
|
192
|
+
when: "at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided",
|
|
175
193
|
},
|
|
176
|
-
// create_issue → notify (
|
|
194
|
+
// create_issue → notify (fixes too complex)
|
|
177
195
|
{
|
|
178
196
|
from: "create_issue",
|
|
179
197
|
to: "notify",
|
|
180
|
-
when: "fix_complexity
|
|
181
|
-
},
|
|
182
|
-
// skip → implement (duplicate exists but has a clear unfixed bug with a simple fix)
|
|
183
|
-
{
|
|
184
|
-
from: "skip",
|
|
185
|
-
to: "implement",
|
|
186
|
-
when: "is_duplicate is true AND the duplicate issue is still open/unfixed AND fix_complexity is simple or moderate AND fix_approach is provided AND dryRun is not true",
|
|
187
|
-
},
|
|
188
|
-
// skip → notify (duplicate was +1'd, no implementation needed or too complex)
|
|
189
|
-
{
|
|
190
|
-
from: "skip",
|
|
191
|
-
to: "notify",
|
|
192
|
-
when: "is_duplicate is true AND (fix_complexity is complex OR no fix_approach OR the issue already has a PR in progress OR dryRun is true), OR severity is low",
|
|
198
|
+
when: "all novel findings have fix_complexity complex, OR no clear fix_approach",
|
|
193
199
|
},
|
|
200
|
+
// skip → notify (nothing to implement — all duplicates +1'd or low priority)
|
|
201
|
+
{ from: "skip", to: "notify" },
|
|
194
202
|
// implement → create_pr (always after successful implementation)
|
|
195
203
|
{ from: "implement", to: "create_pr" },
|
|
196
204
|
// create_pr → notify (always)
|