@sweny-ai/core 0.1.68 → 0.1.70
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/__tests__/report-cloud.test.js +1 -1
- package/dist/cli/check.js +18 -15
- package/dist/cli/config.d.ts +2 -2
- package/dist/cli/config.js +128 -119
- package/dist/cli/config.test.js +11 -7
- package/dist/cli/main.js +18 -16
- package/dist/cli/output.js +3 -3
- package/dist/cli/output.test.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +13 -19
- package/dist/mcp.test.js +12 -12
- package/dist/types.d.ts +2 -1
- package/dist/workflows/browser.js +1 -1
- package/dist/workflows/triage.yml +50 -6
- package/package.json +1 -1
package/dist/cli/check.js
CHANGED
|
@@ -29,21 +29,24 @@ export async function checkProviderConnectivity(config) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
// ── Observability ────────────────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
for (const provider of config.observabilityProviders) {
|
|
33
|
+
const creds = config.observabilityCredentials[provider] ?? {};
|
|
34
|
+
if (provider === "datadog") {
|
|
35
|
+
results.push(await checkDatadog(creds));
|
|
36
|
+
}
|
|
37
|
+
else if (provider === "sentry") {
|
|
38
|
+
results.push(await checkSentry(creds));
|
|
39
|
+
}
|
|
40
|
+
else if (provider === "file") {
|
|
41
|
+
results.push({ name: "Observability (file)", status: "skip", detail: "File provider — no network check needed" });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
results.push({
|
|
45
|
+
name: `Observability (${provider})`,
|
|
46
|
+
status: "skip",
|
|
47
|
+
detail: "Connectivity check not implemented for this provider",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
// ── Issue tracker ────────────────────────────────────────────────────────────
|
|
49
52
|
if (config.issueTrackerProvider === "linear") {
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export interface CliConfig {
|
|
|
7
7
|
claudeOauthToken: string;
|
|
8
8
|
openaiApiKey: string;
|
|
9
9
|
geminiApiKey: string;
|
|
10
|
-
|
|
11
|
-
observabilityCredentials: Record<string, string
|
|
10
|
+
observabilityProviders: string[];
|
|
11
|
+
observabilityCredentials: Record<string, Record<string, string>>;
|
|
12
12
|
issueTrackerProvider: string;
|
|
13
13
|
linearApiKey: string;
|
|
14
14
|
linearTeamId: string;
|
package/dist/cli/config.js
CHANGED
|
@@ -86,7 +86,15 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
86
86
|
return [fileVal];
|
|
87
87
|
return [];
|
|
88
88
|
};
|
|
89
|
-
const
|
|
89
|
+
const obsRaw = options.observabilityProvider || f("observability-provider") || "none";
|
|
90
|
+
const obsProviders = obsRaw
|
|
91
|
+
.split(",")
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter((s) => s && s !== "none");
|
|
94
|
+
const obsCreds = {};
|
|
95
|
+
for (const p of obsProviders) {
|
|
96
|
+
obsCreds[p] = parseObservabilityCredentials(p, options, fileConfig);
|
|
97
|
+
}
|
|
90
98
|
return {
|
|
91
99
|
codingAgentProvider: options.agent || options.codingAgentProvider || f("coding-agent-provider") || "claude",
|
|
92
100
|
// Secrets: env only — never from config file
|
|
@@ -94,8 +102,8 @@ export function parseCliInputs(options, fileConfig = {}) {
|
|
|
94
102
|
claudeOauthToken: env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
|
95
103
|
openaiApiKey: env.OPENAI_API_KEY || "",
|
|
96
104
|
geminiApiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "",
|
|
97
|
-
|
|
98
|
-
observabilityCredentials:
|
|
105
|
+
observabilityProviders: obsProviders,
|
|
106
|
+
observabilityCredentials: obsCreds,
|
|
99
107
|
issueTrackerProvider: options.issueTrackerProvider || f("issue-tracker-provider") || "github-issues",
|
|
100
108
|
linearApiKey: env.LINEAR_API_KEY || "",
|
|
101
109
|
linearTeamId: options.linearTeamId || env.LINEAR_TEAM_ID || f("linear-team-id") || "",
|
|
@@ -218,122 +226,123 @@ export function validateInputs(config) {
|
|
|
218
226
|
if (!config.repository && !allLocal) {
|
|
219
227
|
errors.push("Missing: --repository <owner/repo> or GITHUB_REPOSITORY (could not auto-detect from git remote)");
|
|
220
228
|
}
|
|
221
|
-
// Observability credentials by provider
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
229
|
+
// Observability credentials by provider — validate each configured provider
|
|
230
|
+
for (const provider of config.observabilityProviders) {
|
|
231
|
+
const creds = config.observabilityCredentials[provider] ?? {};
|
|
232
|
+
switch (provider) {
|
|
233
|
+
case "datadog":
|
|
234
|
+
if (!creds.apiKey)
|
|
235
|
+
errors.push("Missing: DD_API_KEY — find API keys at https://app.datadoghq.com/organization-settings/api-keys");
|
|
236
|
+
if (!creds.appKey)
|
|
237
|
+
errors.push("Missing: DD_APP_KEY — find application keys at https://app.datadoghq.com/organization-settings/application-keys");
|
|
238
|
+
break;
|
|
239
|
+
case "sentry":
|
|
240
|
+
if (!creds.authToken)
|
|
241
|
+
errors.push("Missing: SENTRY_AUTH_TOKEN — create a token at https://sentry.io/settings/auth-tokens/");
|
|
242
|
+
if (!creds.organization)
|
|
243
|
+
errors.push("Missing: --sentry-org is required for sentry provider");
|
|
244
|
+
if (!creds.project)
|
|
245
|
+
errors.push("Missing: --sentry-project is required for sentry provider");
|
|
246
|
+
break;
|
|
247
|
+
case "cloudwatch":
|
|
248
|
+
if (!creds.logGroupPrefix)
|
|
249
|
+
errors.push("Missing: --cloudwatch-log-group-prefix is required for cloudwatch provider");
|
|
250
|
+
break;
|
|
251
|
+
case "splunk":
|
|
252
|
+
if (!creds.baseUrl)
|
|
253
|
+
errors.push("Missing: SPLUNK_URL is required for splunk provider");
|
|
254
|
+
if (!creds.token)
|
|
255
|
+
errors.push("Missing: SPLUNK_TOKEN is required for splunk provider");
|
|
256
|
+
break;
|
|
257
|
+
case "elastic":
|
|
258
|
+
if (!creds.baseUrl)
|
|
259
|
+
errors.push("Missing: ELASTIC_URL is required for elastic provider");
|
|
260
|
+
if (!creds.apiKey)
|
|
261
|
+
errors.push("Missing: ELASTIC_API_KEY is required for elastic provider");
|
|
262
|
+
break;
|
|
263
|
+
case "newrelic":
|
|
264
|
+
if (!creds.apiKey)
|
|
265
|
+
errors.push("Missing: NR_API_KEY is required for newrelic provider");
|
|
266
|
+
if (!creds.accountId)
|
|
267
|
+
errors.push("Missing: NR_ACCOUNT_ID is required for newrelic provider");
|
|
268
|
+
break;
|
|
269
|
+
case "loki":
|
|
270
|
+
if (!creds.baseUrl)
|
|
271
|
+
errors.push("Missing: LOKI_URL is required for loki provider");
|
|
272
|
+
break;
|
|
273
|
+
case "file":
|
|
274
|
+
if (!creds.path)
|
|
275
|
+
errors.push("Missing: --log-file <path> is required for file provider");
|
|
276
|
+
break;
|
|
277
|
+
case "prometheus":
|
|
278
|
+
if (!creds.url)
|
|
279
|
+
errors.push("Missing: PROMETHEUS_URL or --prometheus-url is required for prometheus provider");
|
|
280
|
+
break;
|
|
281
|
+
case "pagerduty":
|
|
282
|
+
if (!creds.apiKey)
|
|
283
|
+
errors.push("Missing: PAGERDUTY_API_KEY is required for pagerduty provider");
|
|
284
|
+
break;
|
|
285
|
+
case "heroku":
|
|
286
|
+
if (!creds.apiKey)
|
|
287
|
+
errors.push("Missing: HEROKU_API_KEY is required for heroku provider");
|
|
288
|
+
if (!creds.appName)
|
|
289
|
+
errors.push("Missing: HEROKU_APP_NAME or --heroku-app-name is required for heroku provider");
|
|
290
|
+
break;
|
|
291
|
+
case "opsgenie":
|
|
292
|
+
if (!creds.apiKey)
|
|
293
|
+
errors.push("Missing: OPSGENIE_API_KEY is required for opsgenie provider");
|
|
294
|
+
break;
|
|
295
|
+
case "honeycomb":
|
|
296
|
+
if (!creds.apiKey)
|
|
297
|
+
errors.push("Missing: HONEYCOMB_API_KEY is required for honeycomb provider");
|
|
298
|
+
if (!creds.dataset)
|
|
299
|
+
errors.push("Missing: HONEYCOMB_DATASET is required for honeycomb provider");
|
|
300
|
+
break;
|
|
301
|
+
case "axiom":
|
|
302
|
+
if (!creds.apiToken)
|
|
303
|
+
errors.push("Missing: AXIOM_TOKEN is required for axiom provider");
|
|
304
|
+
if (!creds.dataset)
|
|
305
|
+
errors.push("Missing: AXIOM_DATASET or --axiom-dataset is required for axiom provider");
|
|
306
|
+
break;
|
|
307
|
+
case "betterstack":
|
|
308
|
+
if (!creds.apiToken)
|
|
309
|
+
errors.push("Missing: BETTERSTACK_API_TOKEN, BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN is required for betterstack provider");
|
|
310
|
+
if (!creds.sourceId && !creds.tableName)
|
|
311
|
+
errors.push("Missing: either BETTERSTACK_SOURCE_ID (--betterstack-source-id) or BETTERSTACK_TABLE_NAME (--betterstack-table-name) is required for betterstack provider");
|
|
312
|
+
break;
|
|
313
|
+
case "vercel":
|
|
314
|
+
if (!creds.token)
|
|
315
|
+
errors.push("Missing: VERCEL_TOKEN — create a token at https://vercel.com/account/tokens");
|
|
316
|
+
if (!creds.projectId)
|
|
317
|
+
errors.push("Missing: VERCEL_PROJECT_ID — find it in your Vercel project settings");
|
|
318
|
+
break;
|
|
319
|
+
case "supabase":
|
|
320
|
+
if (!creds.managementApiKey)
|
|
321
|
+
errors.push("Missing: SUPABASE_MANAGEMENT_KEY — create a token at https://supabase.com/dashboard/account/tokens");
|
|
322
|
+
if (!creds.projectRef)
|
|
323
|
+
errors.push("Missing: SUPABASE_PROJECT_REF — find it in your Supabase project settings > General");
|
|
324
|
+
break;
|
|
325
|
+
case "netlify":
|
|
326
|
+
if (!creds.token)
|
|
327
|
+
errors.push("Missing: NETLIFY_TOKEN — create a token at https://app.netlify.com/user/applications#personal-access-tokens");
|
|
328
|
+
if (!creds.siteId)
|
|
329
|
+
errors.push("Missing: NETLIFY_SITE_ID — find it in Site Settings > General > Site details");
|
|
330
|
+
break;
|
|
331
|
+
case "fly":
|
|
332
|
+
if (!creds.token)
|
|
333
|
+
errors.push("Missing: FLY_TOKEN — create a token at https://fly.io/user/personal_access_tokens");
|
|
334
|
+
if (!creds.appName)
|
|
335
|
+
errors.push("Missing: FLY_APP_NAME — the name of your Fly.io application");
|
|
336
|
+
break;
|
|
337
|
+
case "render":
|
|
338
|
+
if (!creds.apiKey)
|
|
339
|
+
errors.push("Missing: RENDER_API_KEY — create an API key at https://dashboard.render.com/u/settings");
|
|
340
|
+
if (!creds.serviceId)
|
|
341
|
+
errors.push("Missing: RENDER_SERVICE_ID — find it in your service's Settings page (srv-...)");
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
errors.push(`Unknown --observability-provider "${provider}". Valid values: none, datadog, sentry, cloudwatch, splunk, elastic, newrelic, loki, prometheus, pagerduty, honeycomb, heroku, opsgenie, axiom, betterstack, vercel, supabase, netlify, fly, render, file`);
|
|
345
|
+
}
|
|
337
346
|
}
|
|
338
347
|
// Issue tracker credentials by provider
|
|
339
348
|
switch (config.issueTrackerProvider) {
|
package/dist/cli/config.test.js
CHANGED
|
@@ -12,8 +12,8 @@ function baseConfig(overrides = {}) {
|
|
|
12
12
|
claudeOauthToken: "",
|
|
13
13
|
openaiApiKey: "",
|
|
14
14
|
geminiApiKey: "",
|
|
15
|
-
|
|
16
|
-
observabilityCredentials: {},
|
|
15
|
+
observabilityProviders: ["file"],
|
|
16
|
+
observabilityCredentials: { file: {} },
|
|
17
17
|
issueTrackerProvider: "file",
|
|
18
18
|
linearApiKey: "",
|
|
19
19
|
linearTeamId: "",
|
|
@@ -114,23 +114,27 @@ describe("validateInputs — notification provider", () => {
|
|
|
114
114
|
});
|
|
115
115
|
describe("validateInputs — observability provider", () => {
|
|
116
116
|
it("accepts 'none' without requiring any credentials", () => {
|
|
117
|
-
const errors = validateInputs(baseConfig({
|
|
117
|
+
const errors = validateInputs(baseConfig({ observabilityProviders: [], observabilityCredentials: {} }));
|
|
118
118
|
expect(errors.filter((e) => e.includes("DD_") || e.includes("observability"))).toEqual([]);
|
|
119
119
|
});
|
|
120
120
|
it("requires DD keys when explicitly set to datadog", () => {
|
|
121
|
-
const errors = validateInputs(baseConfig({
|
|
121
|
+
const errors = validateInputs(baseConfig({ observabilityProviders: ["datadog"], observabilityCredentials: {} }));
|
|
122
122
|
expect(errors.some((e) => e.includes("DD_API_KEY"))).toBe(true);
|
|
123
123
|
expect(errors.some((e) => e.includes("DD_APP_KEY"))).toBe(true);
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
describe("parseCliInputs — observability provider default", () => {
|
|
127
|
-
it("defaults to
|
|
127
|
+
it("defaults to empty array when no provider is configured", () => {
|
|
128
128
|
const config = parseCliInputs({}, {});
|
|
129
|
-
expect(config.
|
|
129
|
+
expect(config.observabilityProviders).toEqual([]);
|
|
130
130
|
});
|
|
131
131
|
it("respects explicit datadog provider", () => {
|
|
132
132
|
const config = parseCliInputs({ observabilityProvider: "datadog" }, {});
|
|
133
|
-
expect(config.
|
|
133
|
+
expect(config.observabilityProviders).toEqual(["datadog"]);
|
|
134
|
+
});
|
|
135
|
+
it("supports comma-separated multiple providers", () => {
|
|
136
|
+
const config = parseCliInputs({ observabilityProvider: "loki,sentry" }, {});
|
|
137
|
+
expect(config.observabilityProviders).toEqual(["loki", "sentry"]);
|
|
134
138
|
});
|
|
135
139
|
});
|
|
136
140
|
describe("fetch.auth + offline parsing", () => {
|
package/dist/cli/main.js
CHANGED
|
@@ -111,7 +111,7 @@ function buildMcpAutoConfig(config) {
|
|
|
111
111
|
return {
|
|
112
112
|
sourceControlProvider: config.sourceControlProvider,
|
|
113
113
|
issueTrackerProvider: config.issueTrackerProvider,
|
|
114
|
-
|
|
114
|
+
observabilityProviders: config.observabilityProviders,
|
|
115
115
|
credentials: buildCredentialMap(),
|
|
116
116
|
workspaceTools: config.workspaceTools,
|
|
117
117
|
userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
|
|
@@ -122,14 +122,15 @@ function buildMcpAutoConfig(config) {
|
|
|
122
122
|
*/
|
|
123
123
|
function buildProviderCtx(config, mcpServers) {
|
|
124
124
|
const extras = {};
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const bsCreds = config.observabilityCredentials["betterstack"];
|
|
126
|
+
if (bsCreds?.sourceId) {
|
|
127
|
+
extras["BetterStack source ID"] = bsCreds.sourceId;
|
|
127
128
|
}
|
|
128
|
-
if (
|
|
129
|
-
extras["BetterStack table name"] =
|
|
129
|
+
if (bsCreds?.tableName) {
|
|
130
|
+
extras["BetterStack table name"] = bsCreds.tableName;
|
|
130
131
|
}
|
|
131
132
|
return buildProviderContext({
|
|
132
|
-
|
|
133
|
+
observabilityProviders: config.observabilityProviders,
|
|
133
134
|
issueTrackerProvider: config.issueTrackerProvider,
|
|
134
135
|
sourceControlProvider: config.sourceControlProvider,
|
|
135
136
|
mcpServers: Object.keys(mcpServers),
|
|
@@ -306,6 +307,7 @@ triageCmd.action(async (options) => {
|
|
|
306
307
|
if (config.additionalInstructions)
|
|
307
308
|
contextParts.push(config.additionalInstructions);
|
|
308
309
|
const fullContext = [contextParts.join("\n\n"), context].filter(Boolean).join("\n\n---\n\n");
|
|
310
|
+
const bsCreds = config.observabilityCredentials["betterstack"];
|
|
309
311
|
const workflowInput = {
|
|
310
312
|
timeRange: config.timeRange,
|
|
311
313
|
severityFocus: config.severityFocus,
|
|
@@ -320,12 +322,12 @@ triageCmd.action(async (options) => {
|
|
|
320
322
|
issueOverride: config.issueOverride,
|
|
321
323
|
noveltyMode: config.noveltyMode,
|
|
322
324
|
reviewMode: config.reviewMode,
|
|
323
|
-
|
|
324
|
-
...(
|
|
325
|
-
betterstackSourceId:
|
|
325
|
+
observabilityProviders: config.observabilityProviders,
|
|
326
|
+
...(bsCreds?.sourceId && {
|
|
327
|
+
betterstackSourceId: bsCreds.sourceId,
|
|
326
328
|
}),
|
|
327
|
-
...(
|
|
328
|
-
betterstackTableName:
|
|
329
|
+
...(bsCreds?.tableName && {
|
|
330
|
+
betterstackTableName: bsCreds.tableName,
|
|
329
331
|
}),
|
|
330
332
|
// Structured rules/context for executor (URLs resolved eagerly by loadAdditionalContext)
|
|
331
333
|
rules,
|
|
@@ -665,12 +667,12 @@ export async function workflowRunAction(file, options) {
|
|
|
665
667
|
baseBranch: config.baseBranch,
|
|
666
668
|
prLabels: config.prLabels,
|
|
667
669
|
additionalInstructions: config.additionalInstructions,
|
|
668
|
-
|
|
669
|
-
...(config.observabilityCredentials
|
|
670
|
-
betterstackSourceId: config.observabilityCredentials.sourceId,
|
|
670
|
+
observabilityProviders: config.observabilityProviders,
|
|
671
|
+
...(config.observabilityCredentials["betterstack"]?.sourceId && {
|
|
672
|
+
betterstackSourceId: config.observabilityCredentials["betterstack"].sourceId,
|
|
671
673
|
}),
|
|
672
|
-
...(config.observabilityCredentials
|
|
673
|
-
betterstackTableName: config.observabilityCredentials.tableName,
|
|
674
|
+
...(config.observabilityCredentials["betterstack"]?.tableName && {
|
|
675
|
+
betterstackTableName: config.observabilityCredentials["betterstack"].tableName,
|
|
674
676
|
}),
|
|
675
677
|
context: buildProviderCtx(config, mcpServers),
|
|
676
678
|
};
|
package/dist/cli/output.js
CHANGED
|
@@ -89,7 +89,7 @@ export function formatBanner(config, version) {
|
|
|
89
89
|
const rows = [
|
|
90
90
|
`${c.subtle("Repository")}${" ".repeat(6)}${chalk.white(config.repository)}`,
|
|
91
91
|
`${c.subtle("Agent")}${" ".repeat(11)}${chalk.white(config.codingAgentProvider)}`,
|
|
92
|
-
`${c.subtle("Observability")}${" ".repeat(3)}${chalk.white(config.
|
|
92
|
+
`${c.subtle("Observability")}${" ".repeat(3)}${chalk.white(config.observabilityProviders.join(", "))}`,
|
|
93
93
|
`${c.subtle("Issue tracker")}${" ".repeat(3)}${chalk.white(config.issueTrackerProvider)}`,
|
|
94
94
|
`${c.subtle("Time range")}${" ".repeat(6)}${chalk.white(config.timeRange)}`,
|
|
95
95
|
`${c.subtle("Mode")}${" ".repeat(12)}${mode}`,
|
|
@@ -460,8 +460,8 @@ export function formatDagResultMarkdown(results, durationMs, config, options = {
|
|
|
460
460
|
const rows = [];
|
|
461
461
|
if (config?.repository)
|
|
462
462
|
rows.push(["Repository", `\`${config.repository}\``]);
|
|
463
|
-
if (config?.
|
|
464
|
-
rows.push(["Observability", config.
|
|
463
|
+
if (config?.observabilityProviders?.length)
|
|
464
|
+
rows.push(["Observability", config.observabilityProviders.join(", ")]);
|
|
465
465
|
if (config?.issueTrackerProvider)
|
|
466
466
|
rows.push(["Issue tracker", config.issueTrackerProvider]);
|
|
467
467
|
if (config?.timeRange)
|
package/dist/cli/output.test.js
CHANGED
|
@@ -12,7 +12,7 @@ function minimalConfig(overrides = {}) {
|
|
|
12
12
|
timeRange: "6h",
|
|
13
13
|
serviceFilter: "permit-service",
|
|
14
14
|
repository: "acme/app",
|
|
15
|
-
|
|
15
|
+
observabilityProviders: ["betterstack"],
|
|
16
16
|
issueTrackerProvider: "linear",
|
|
17
17
|
dryRun: false,
|
|
18
18
|
...overrides,
|
package/dist/mcp.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function buildSkillMcpServers(opts: SkillMcpOptions): Record<stri
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function buildAutoMcpServers(config: McpAutoConfig): Record<string, McpServerConfig>;
|
|
37
37
|
export interface ProviderContextOptions {
|
|
38
|
-
|
|
38
|
+
observabilityProviders?: string[];
|
|
39
39
|
issueTrackerProvider?: string;
|
|
40
40
|
sourceControlProvider?: string;
|
|
41
41
|
/** Which MCP servers were actually injected (keys from buildAutoMcpServers) */
|
package/dist/mcp.js
CHANGED
|
@@ -230,10 +230,14 @@ export function buildAutoMcpServers(config) {
|
|
|
230
230
|
},
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
|
+
// ── Observability MCP servers ──────────────────────────────────
|
|
234
|
+
// Inject MCP for every configured observability provider whose credentials
|
|
235
|
+
// are present. Multiple providers are supported (e.g. "loki,sentry").
|
|
236
|
+
const obsProviders = new Set(config.observabilityProviders ?? []);
|
|
233
237
|
// Datadog MCP — HTTP transport.
|
|
234
238
|
const ddApiKey = creds.DD_API_KEY;
|
|
235
239
|
const ddAppKey = creds.DD_APP_KEY;
|
|
236
|
-
if (
|
|
240
|
+
if (obsProviders.has("datadog") && ddApiKey && ddAppKey) {
|
|
237
241
|
auto["datadog"] = {
|
|
238
242
|
type: "http",
|
|
239
243
|
url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
|
|
@@ -242,7 +246,7 @@ export function buildAutoMcpServers(config) {
|
|
|
242
246
|
}
|
|
243
247
|
// Sentry MCP — @sentry/mcp-server reads SENTRY_ACCESS_TOKEN (not SENTRY_AUTH_TOKEN).
|
|
244
248
|
const sentryAuthToken = creds.SENTRY_AUTH_TOKEN;
|
|
245
|
-
if (
|
|
249
|
+
if (obsProviders.has("sentry") && sentryAuthToken) {
|
|
246
250
|
const sentryEnv = { SENTRY_ACCESS_TOKEN: sentryAuthToken };
|
|
247
251
|
const sentryUrl = creds.SENTRY_URL;
|
|
248
252
|
if (sentryUrl && sentryUrl !== "https://sentry.io") {
|
|
@@ -261,10 +265,8 @@ export function buildAutoMcpServers(config) {
|
|
|
261
265
|
};
|
|
262
266
|
}
|
|
263
267
|
// New Relic MCP — HTTP transport; region-aware endpoint.
|
|
264
|
-
// Header key is "Api-Key" (not "Authorization"), unique to New Relic's MCP.
|
|
265
|
-
// Trailing slash is intentional — New Relic's MCP spec requires it.
|
|
266
268
|
const nrApiKey = creds.NEW_RELIC_API_KEY;
|
|
267
|
-
if (
|
|
269
|
+
if (obsProviders.has("newrelic") && nrApiKey) {
|
|
268
270
|
const nrRegion = creds.NEW_RELIC_REGION;
|
|
269
271
|
const nrEndpoint = nrRegion === "eu" ? "https://mcp.eu.newrelic.com/mcp/" : "https://mcp.newrelic.com/mcp/";
|
|
270
272
|
auto["newrelic"] = {
|
|
@@ -274,13 +276,8 @@ export function buildAutoMcpServers(config) {
|
|
|
274
276
|
};
|
|
275
277
|
}
|
|
276
278
|
// Better Stack MCP — HTTP remote MCP; Bearer token auth.
|
|
277
|
-
// BetterStack uses separate tokens for Uptime vs Telemetry APIs.
|
|
278
|
-
// The MCP server accepts the telemetry token for log/metric queries.
|
|
279
|
-
// Accept: BETTERSTACK_API_TOKEN (legacy), BETTERSTACK_TELEMETRY_TOKEN, or BETTERSTACK_UPTIME_TOKEN.
|
|
280
|
-
// Injected whenever any token is present (not just when it's the primary provider)
|
|
281
|
-
// because BetterStack logs complement any primary observability provider.
|
|
282
279
|
const bsApiToken = creds.BETTERSTACK_API_TOKEN || creds.BETTERSTACK_TELEMETRY_TOKEN || creds.BETTERSTACK_UPTIME_TOKEN;
|
|
283
|
-
if (bsApiToken) {
|
|
280
|
+
if (obsProviders.has("betterstack") && bsApiToken) {
|
|
284
281
|
auto["betterstack"] = {
|
|
285
282
|
type: "http",
|
|
286
283
|
url: "https://mcp.betterstack.com",
|
|
@@ -361,16 +358,13 @@ export function buildAutoMcpServers(config) {
|
|
|
361
358
|
*/
|
|
362
359
|
export function buildProviderContext(opts) {
|
|
363
360
|
const lines = ["## Available Providers & Tools", ""];
|
|
364
|
-
// Observability
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
// Observability — list all configured providers
|
|
362
|
+
const obsProviders = opts.observabilityProviders ?? [];
|
|
363
|
+
for (const provider of obsProviders) {
|
|
364
|
+
const mcpNote = opts.mcpServers.includes(provider)
|
|
367
365
|
? ` (available via MCP — use its tools to query logs, errors, and metrics)`
|
|
368
366
|
: "";
|
|
369
|
-
lines.push(`- **Observability**: ${
|
|
370
|
-
}
|
|
371
|
-
// BetterStack as secondary (token present but not primary provider)
|
|
372
|
-
if (opts.observabilityProvider !== "betterstack" && opts.mcpServers.includes("betterstack")) {
|
|
373
|
-
lines.push(`- **Logs**: betterstack (available via MCP — use its tools to query logs)`);
|
|
367
|
+
lines.push(`- **Observability**: ${provider}${mcpNote}`);
|
|
374
368
|
}
|
|
375
369
|
// Issue tracker
|
|
376
370
|
if (opts.issueTrackerProvider) {
|
package/dist/mcp.test.js
CHANGED
|
@@ -47,7 +47,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
47
47
|
// ── Datadog MCP ─────────────────────────────────────────────────
|
|
48
48
|
it("injects Datadog MCP (HTTP) when observabilityProvider is datadog with DD_API_KEY + DD_APP_KEY", () => {
|
|
49
49
|
const result = buildAutoMcpServers(cfg({
|
|
50
|
-
|
|
50
|
+
observabilityProviders: ["datadog"],
|
|
51
51
|
credentials: { DD_API_KEY: "dd_api", DD_APP_KEY: "dd_app" },
|
|
52
52
|
}));
|
|
53
53
|
expect(result["datadog"]).toEqual({
|
|
@@ -57,12 +57,12 @@ describe("buildAutoMcpServers", () => {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
it("does NOT inject Datadog MCP when DD_APP_KEY is missing", () => {
|
|
60
|
-
const result = buildAutoMcpServers(cfg({
|
|
60
|
+
const result = buildAutoMcpServers(cfg({ observabilityProviders: ["datadog"], credentials: { DD_API_KEY: "dd_api" } }));
|
|
61
61
|
expect(result["datadog"]).toBeUndefined();
|
|
62
62
|
});
|
|
63
63
|
// ── Sentry MCP ──────────────────────────────────────────────────
|
|
64
64
|
it("injects Sentry MCP (stdio) when observabilityProvider is sentry with SENTRY_AUTH_TOKEN", () => {
|
|
65
|
-
const result = buildAutoMcpServers(cfg({
|
|
65
|
+
const result = buildAutoMcpServers(cfg({ observabilityProviders: ["sentry"], credentials: { SENTRY_AUTH_TOKEN: "sntryu_abc" } }));
|
|
66
66
|
expect(result["sentry"]).toEqual({
|
|
67
67
|
type: "stdio",
|
|
68
68
|
command: "npx",
|
|
@@ -72,7 +72,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
72
72
|
});
|
|
73
73
|
it("injects Sentry MCP with self-hosted SENTRY_HOST from SENTRY_URL", () => {
|
|
74
74
|
const result = buildAutoMcpServers(cfg({
|
|
75
|
-
|
|
75
|
+
observabilityProviders: ["sentry"],
|
|
76
76
|
credentials: {
|
|
77
77
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
78
78
|
SENTRY_URL: "https://sentry.mycompany.com",
|
|
@@ -85,7 +85,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
85
85
|
});
|
|
86
86
|
it("does NOT set SENTRY_HOST when SENTRY_URL is sentry.io", () => {
|
|
87
87
|
const result = buildAutoMcpServers(cfg({
|
|
88
|
-
|
|
88
|
+
observabilityProviders: ["sentry"],
|
|
89
89
|
credentials: {
|
|
90
90
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
91
91
|
SENTRY_URL: "https://sentry.io",
|
|
@@ -95,7 +95,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
95
95
|
});
|
|
96
96
|
it("handles malformed SENTRY_URL gracefully (no SENTRY_HOST set)", () => {
|
|
97
97
|
const result = buildAutoMcpServers(cfg({
|
|
98
|
-
|
|
98
|
+
observabilityProviders: ["sentry"],
|
|
99
99
|
credentials: {
|
|
100
100
|
SENTRY_AUTH_TOKEN: "sntryu_abc",
|
|
101
101
|
SENTRY_URL: "not-a-url",
|
|
@@ -143,7 +143,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
143
143
|
// ── New Relic MCP ───────────────────────────────────────────────
|
|
144
144
|
it("injects New Relic MCP with EU region endpoint", () => {
|
|
145
145
|
const result = buildAutoMcpServers(cfg({
|
|
146
|
-
|
|
146
|
+
observabilityProviders: ["newrelic"],
|
|
147
147
|
credentials: { NEW_RELIC_API_KEY: "NRAK-abc", NEW_RELIC_REGION: "eu" },
|
|
148
148
|
}));
|
|
149
149
|
expect(result["newrelic"]).toEqual({
|
|
@@ -154,7 +154,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
154
154
|
});
|
|
155
155
|
it("injects New Relic MCP with US (default) region endpoint", () => {
|
|
156
156
|
const result = buildAutoMcpServers(cfg({
|
|
157
|
-
|
|
157
|
+
observabilityProviders: ["newrelic"],
|
|
158
158
|
credentials: { NEW_RELIC_API_KEY: "NRAK-abc" },
|
|
159
159
|
}));
|
|
160
160
|
expect(result["newrelic"]).toEqual({
|
|
@@ -166,7 +166,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
166
166
|
// ── Better Stack MCP ────────────────────────────────────────────
|
|
167
167
|
it("injects Better Stack MCP (HTTP) with Bearer token", () => {
|
|
168
168
|
const result = buildAutoMcpServers(cfg({
|
|
169
|
-
|
|
169
|
+
observabilityProviders: ["betterstack"],
|
|
170
170
|
credentials: { BETTERSTACK_API_TOKEN: "bst_abc" },
|
|
171
171
|
}));
|
|
172
172
|
expect(result["betterstack"]).toEqual({
|
|
@@ -175,9 +175,9 @@ describe("buildAutoMcpServers", () => {
|
|
|
175
175
|
headers: { Authorization: "Bearer bst_abc" },
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
|
-
it("injects
|
|
178
|
+
it("injects multiple observability MCPs when both are configured", () => {
|
|
179
179
|
const result = buildAutoMcpServers(cfg({
|
|
180
|
-
|
|
180
|
+
observabilityProviders: ["sentry", "betterstack"],
|
|
181
181
|
credentials: { BETTERSTACK_API_TOKEN: "bst_xyz", SENTRY_AUTH_TOKEN: "sntrx_test" },
|
|
182
182
|
}));
|
|
183
183
|
expect(result["betterstack"]).toEqual({
|
|
@@ -329,7 +329,7 @@ describe("buildAutoMcpServers", () => {
|
|
|
329
329
|
const result = buildAutoMcpServers(cfg({
|
|
330
330
|
sourceControlProvider: "github",
|
|
331
331
|
issueTrackerProvider: "linear",
|
|
332
|
-
|
|
332
|
+
observabilityProviders: ["datadog"],
|
|
333
333
|
credentials: {
|
|
334
334
|
GITHUB_TOKEN: "ghp_abc",
|
|
335
335
|
LINEAR_API_KEY: "lin_key_abc",
|
package/dist/types.d.ts
CHANGED
|
@@ -216,7 +216,8 @@ export interface McpServerConfig {
|
|
|
216
216
|
export interface McpAutoConfig {
|
|
217
217
|
sourceControlProvider?: string;
|
|
218
218
|
issueTrackerProvider?: string;
|
|
219
|
-
|
|
219
|
+
/** One or more observability providers (e.g. ["loki", "sentry"]). */
|
|
220
|
+
observabilityProviders?: string[];
|
|
220
221
|
credentials: Record<string, string>;
|
|
221
222
|
workspaceTools?: string[];
|
|
222
223
|
userMcpServers?: Record<string, McpServerConfig>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const triageWorkflow = {"id":"triage","name":"Alert Triage","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","nodes":{"gather":{"name":"Gather Context","instruction":"You are investigating a production alert. Gather all relevant context using the available tools:\n\n1. **Observability**: Pull error details, stack traces, recent logs, metrics, and active incidents around the time of the alert. Use whichever observability tools are available to you.\n2. **Source control**: Check recent commits, pull requests, and deploys that might be related.\n3. **Issue tracker**: Search for similar past issues or known problems.\n\nBe thorough — the investigation step depends on complete context. Use every tool available to you.","skills":["github","sentry","datadog","betterstack","linear"]},"investigate":{"name":"Root Cause Analysis","instruction":"Based on the gathered context, classify every distinct issue you found into one of two buckets: **novel** or **duplicate**.\n\nFor EACH issue found:\n1. Identify the root cause and affected code/service.\n2. Assess severity: critical (service down), high (major feature broken), medium (degraded), low (cosmetic/minor).\n3. Assess fix complexity: \"simple\" (a few lines, clear change), \"moderate\" (multiple files but well-understood), or \"complex\" (architectural, risky, or unclear).\n4. **Novelty check (REQUIRED):** Filing a duplicate ticket is worse than under-reporting — the team inherits sprawl that humans have to clean up later. Complete EVERY step below before classifying anything as novel.\n4a. **Search broadly.** Run multiple queries against the issue tracker (BOTH open AND closed): the affected file or module path, the error message or log string verbatim, the feature or service name, and the symptom family (e.g. \"startup crash\", \"data dropped silently\"). Do not rely on a single keyword — a narrow search is how duplicates slip through.\n4b. **Read the full body of every promising candidate — do NOT match on title alone.** For each top match, open the issue and read its entire description, including any \"Recommended Fix\", \"Proposed Fix\", \"Action Items\", or \"Acceptance Criteria\" section. An existing open issue whose recommended fix already prescribes the change you were about to propose IS a duplicate, even if the symptom you just observed is a new instance of the same underlying bug.\n4c. **Check for meta-issues and bug families.** If an existing ticket names a pattern that your finding is another instance of (same root cause, same function/module, different input or caller), treat your finding as a sibling of that family and comment on the existing ticket. Do not file a parallel ticket per instance — that is the exact pattern that produces backlog sprawl.\n4d. **When uncertain, comment don't file.** If you are less than ~80% confident a finding is truly novel after steps 4a–4c, classify it as a duplicate and add a \"+1 — another instance\" comment to the closest existing issue with the new context. Over-reporting duplicates is cheap; ticket sprawl is expensive.\n\n - If matched → **duplicate**. Record the existing issue ID in `duplicate_of`.\n - If no match after 4a–4d → **novel**.\n\n**Output rules:**\n- `findings`: array of ALL issues found (both novel and duplicate).\n- `novel_count`: how many findings are novel (not duplicates).\n- `highest_severity`: the highest severity across ALL findings.\n- `recommendation`: what should happen next.\n\nDownstream nodes will act ONLY on novel findings. Duplicates will be +1'd automatically.","skills":["github","linear"],"output":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","description":"Short description of the issue"},"root_cause":{"type":"string"},"severity":{"type":"string","enum":["critical","high","medium","low"]},"affected_services":{"type":"array","items":{"type":"string"}},"is_duplicate":{"type":"boolean"},"duplicate_of":{"type":"string","description":"Existing issue ID/URL if duplicate"},"fix_approach":{"type":"string"},"fix_complexity":{"type":"string","enum":["simple","moderate","complex"]}},"required":["title","root_cause","severity","is_duplicate"]}},"novel_count":{"type":"number","description":"Count of novel (non-duplicate) findings"},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","highest_severity","recommendation"]}},"create_issue":{"name":"Create Issues & Triage Duplicates","instruction":"Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.\n\n**For each NOVEL finding** (is_duplicate = false):\n1. Create a new issue with a clear, actionable title.\n2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n3. Add appropriate labels (bug, severity level, affected service).\n4. Link to relevant commits, PRs, or existing issues.\n\n**For each DUPLICATE finding** (is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. 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.\n3. 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.)\n4. If the existing issue is closed/done, reopen it.\n\nIf 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.\n\nUse the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues) — do NOT fall back to a different tracker. Output the created/updated issue identifiers.","skills":["linear","github"]},"skip":{"name":"Skip — All Duplicates or Low Priority","instruction":"Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.\n\nFor each **duplicate** finding (check the findings array for items where is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. 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.\n3. 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.)\n4. If the issue is closed/done, reopen it.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation:\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n2. Read the relevant source files to understand the current code.\n3. Make the necessary code changes — fix the bug, nothing more.\n4. Run any existing tests if available to verify the fix doesn't break anything.\n5. Stage and commit with a clear commit message referencing the issue.\n\nKeep changes minimal and focused. Do not refactor surrounding code or add unrelated improvements.\n\nIf the fix turns out to be more complex than expected, stop and explain why — do not force a bad fix.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix:\n\n1. Push the branch to the remote.\n2. Create a PR with a clear title referencing the issue (e.g. \"[OFF-1020] fix: guard empty pdf_texts before access\").\n3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue.\n4. Add appropriate labels if available.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL and number.","skills":["github"]},"notify":{"name":"Notify Team","instruction":"Send a notification summarizing the triage result:\n\n1. Include: alert summary, severity, root cause (1-2 sentences), and links to any created issues or PRs.\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"]}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":"novel_count is greater than 0 AND highest_severity is medium or higher"},{"from":"investigate","to":"skip","when":"novel_count is 0, OR highest_severity is low"},{"from":"create_issue","to":"implement","when":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","when":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"create_pr"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
1
|
+
export const triageWorkflow = {"id":"triage","name":"Alert Triage","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","nodes":{"gather":{"name":"Gather Context","instruction":"You are investigating a production alert. Gather all relevant context using the available tools:\n\n1. **Observability**: Pull error details, stack traces, recent logs, metrics, and active incidents around the time of the alert. Use whichever observability tools are available to you.\n2. **Source control**: Check recent commits, pull requests, and deploys that might be related.\n3. **Issue tracker**: Search for similar past issues or known problems.\n\nBe thorough — the investigation step depends on complete context. Use every tool available to you.","skills":["github","sentry","datadog","betterstack","linear"]},"investigate":{"name":"Root Cause Analysis","instruction":"Based on the gathered context, classify every distinct issue you found into one of two buckets: **novel** or **duplicate**.\n\nFor EACH issue found:\n1. Identify the root cause and affected code/service.\n2. Assess severity: critical (service down), high (major feature broken), medium (degraded), low (cosmetic/minor).\n3. Assess fix complexity: \"simple\" (a few lines, clear change), \"moderate\" (multiple files but well-understood), or \"complex\" (architectural, risky, or unclear).\n4. **Novelty check (REQUIRED):** Filing a duplicate ticket is worse than under-reporting — the team inherits sprawl that humans have to clean up later. Complete EVERY step below before classifying anything as novel.\n4a. **Search broadly.** Run multiple queries against the issue tracker (BOTH open AND closed): the affected file or module path, the error message or log string verbatim, the feature or service name, and the symptom family (e.g. \"startup crash\", \"data dropped silently\"). Do not rely on a single keyword — a narrow search is how duplicates slip through.\n4b. **Read the full body of every promising candidate — do NOT match on title alone.** For each top match, open the issue and read its entire description, including any \"Recommended Fix\", \"Proposed Fix\", \"Action Items\", or \"Acceptance Criteria\" section. An existing open issue whose recommended fix already prescribes the change you were about to propose IS a duplicate, even if the symptom you just observed is a new instance of the same underlying bug.\n4c. **Check for meta-issues and bug families.** If an existing ticket names a pattern that your finding is another instance of (same root cause, same function/module, different input or caller), treat your finding as a sibling of that family and comment on the existing ticket. Do not file a parallel ticket per instance — that is the exact pattern that produces backlog sprawl.\n4d. **When uncertain, comment don't file.** If you are less than ~80% confident a finding is truly novel after steps 4a–4c, classify it as a duplicate and add a \"+1 — another instance\" comment to the closest existing issue with the new context. Over-reporting duplicates is cheap; ticket sprawl is expensive.\n\n - If matched → **duplicate**. Record the existing issue ID in `duplicate_of`.\n - If no match after 4a–4d → **novel**.\n\n**Output rules:**\n- `findings`: array of ALL issues found (both novel and duplicate).\n- `novel_count`: how many findings are novel (not duplicates).\n- `highest_severity`: the highest severity across ALL findings.\n- `recommendation`: what should happen next.\n\nDownstream nodes will act ONLY on novel findings. Duplicates will be +1'd automatically.","skills":["github","linear"],"output":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","description":"Short description of the issue"},"root_cause":{"type":"string"},"severity":{"type":"string","enum":["critical","high","medium","low"]},"affected_services":{"type":"array","items":{"type":"string"}},"is_duplicate":{"type":"boolean"},"duplicate_of":{"type":"string","description":"Existing issue ID/URL if duplicate"},"fix_approach":{"type":"string"},"fix_complexity":{"type":"string","enum":["simple","moderate","complex"]}},"required":["title","root_cause","severity","is_duplicate"]}},"novel_count":{"type":"number","description":"Count of novel (non-duplicate) findings"},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","highest_severity","recommendation"]}},"create_issue":{"name":"Create Issues & Triage Duplicates","instruction":"Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.\n\n**For each NOVEL finding** (is_duplicate = false):\n1. Create a new issue with a clear, actionable title.\n2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n3. Add appropriate labels (bug, severity level, affected service).\n4. Link to relevant commits, PRs, or existing issues.\n\n**For each DUPLICATE finding** (is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. 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.\n3. 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.)\n4. If the existing issue is closed/done, reopen it.\n\nIf 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.\n\nUse the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues) — do NOT fall back to a different tracker. Output the created/updated issue identifiers.","skills":["linear","github"],"output":{"type":"object","properties":{"issueIdentifier":{"type":"string","description":"Primary issue identifier (e.g. OFF-1234). This is the first novel issue created — used by downstream nodes for branch naming and PR linking."},"issueTitle":{"type":"string","description":"Title of the primary issue created"},"issueUrl":{"type":"string","description":"URL of the primary issue created"},"issues":{"type":"array","description":"All issues created or updated during this step","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"action":{"type":"string","enum":["created","updated","reopened"]}},"required":["identifier","action"]}}},"required":["issueIdentifier","issueTitle","issueUrl","issues"]}},"skip":{"name":"Skip — All Duplicates or Low Priority","instruction":"Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.\n\nFor each **duplicate** finding (check the findings array for items where is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. 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.\n3. 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.)\n4. If the issue is closed/done, reopen it.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation. The issue created in the previous step is in your context — use its identifier for branch naming and commit messages.\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n **Branch name MUST include the issue identifier** (e.g. context.create_issue.issueIdentifier).\n Format: lowercase the identifier and use it as a prefix, e.g. `off-1234-fix-null-check`.\n If the repository uses a different branch convention (check context.branchPattern), follow that instead.\n2. Read the relevant source files to understand the current code.\n3. Make the necessary code changes — fix the bug, nothing more.\n4. Run any existing tests if available to verify the fix doesn't break anything.\n5. Stage and commit with a clear commit message. Start the message with the issue identifier in brackets,\n e.g. `[OFF-1234] fix: guard null projectId before database lookup`.\n\nKeep changes minimal and focused. Do not refactor surrounding code or add unrelated improvements.\n\nIf the fix turns out to be more complex than expected, stop and explain why — do not force a bad fix.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix. The issue identifier and URL are in your context from the create_issue step.\n\n1. Push the branch to the remote.\n2. Create a PR with a title that starts with the issue identifier in brackets. Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.\n3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue (context.create_issue.issueUrl). If the investigation found a Sentry issue, include \"Fixes {sentryShortId}\" in the PR body so Sentry auto-resolves on merge.\n4. Add appropriate labels if available.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL and number.","skills":["github"]},"notify":{"name":"Notify Team","instruction":"Send a notification summarizing the triage result:\n\n1. Include: alert summary, severity, root cause (1-2 sentences), and links to any created issues or PRs.\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"]}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":"novel_count is greater than 0 AND highest_severity is medium or higher"},{"from":"investigate","to":"skip","when":"novel_count is 0, OR highest_severity is low"},{"from":"create_issue","to":"implement","when":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","when":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"create_pr"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
2
2
|
export const implementWorkflow = {"id":"implement","name":"Implement Fix","description":"Implement a code fix for an issue and open a pull request","entry":"analyze","nodes":{"analyze":{"name":"Analyze Issue","instruction":"Read the issue details and understand what needs to be fixed:\n\n1. Fetch the issue from the tracker (GitHub or Linear).\n2. Read the relevant source files to understand the current code.\n3. Identify the exact files and lines that need to change.\n4. Plan the fix approach.\n\nOutput a clear analysis of what needs to change and why.","skills":["github","linear"],"output":{"type":"object","properties":{"issue_summary":{"type":"string"},"files_to_change":{"type":"array","items":{"type":"string"}},"fix_plan":{"type":"string"},"risk_level":{"type":"string","enum":["low","medium","high"]}},"required":["issue_summary","fix_plan"]}},"implement":{"name":"Implement Fix","instruction":"Implement the planned fix:\n\n1. Create a feature branch.\n2. Make the necessary code changes.\n3. Ensure changes are minimal and focused — fix the bug, nothing more.\n4. Stage and commit with a clear commit message referencing the issue.\n\nIf the fix is too risky or complex, explain why and skip.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix:\n\n1. Push the branch to the remote.\n2. Create a PR with a clear title and description.\n3. Reference the original issue in the PR body.\n4. Add appropriate reviewers or labels if possible.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL.","skills":["github"]},"notify":{"name":"Notify","instruction":"Send a notification about the implementation result:\n\n1. Include: issue reference, PR link, brief summary of changes.\n2. Keep it concise — one message, not a wall of text.","skills":["slack","notification"]},"skip":{"name":"Skip — Too Complex","instruction":"The fix was determined to be too complex or risky for automated implementation.\nAdd a comment to the issue explaining what was found and why it needs manual attention.","skills":["github","linear"]}},"edges":[{"from":"analyze","to":"implement","when":"Fix risk level is low or medium and a clear plan exists"},{"from":"analyze","to":"skip","when":"Fix is too complex, risky, or unclear"},{"from":"implement","to":"create_pr"},{"from":"skip","to":"notify"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
3
3
|
export const seedContentWorkflow = {"id":"seed-content","name":"Content Generation Pipeline","description":"Audit content gaps, generate educational content, validate quality, and publish to Supabase","entry":"audit","nodes":{"audit":{"name":"Audit Content Gaps","instruction":"You are auditing an educational platform's content library.\n\nUse supabase_count to check how many items exist for each combination of:\n- Content types: worksheets, games, lessons\n- Subjects: math, science, english, history, geography, economics, music, art, life-skills, foreign-language\n- Difficulties: beginner, easy, medium, hard\n\nAlso check the input parameters — if the user specified a subject, topic, difficulty, and count,\nskip the full audit and just report what they want generated.\n\nReturn a structured content plan: what to generate, how many, at what difficulty and grade level.\nPrioritize gaps where a subject has ZERO content of a given type.","skills":["supabase"],"output":{"type":"object","properties":{"plan":{"type":"array","items":{"type":"object","properties":{"contentType":{"type":"string"},"subject":{"type":"string"},"topic":{"type":"string"},"difficulty":{"type":"string"},"gradeLevel":{"type":"string"},"count":{"type":"number"},"reason":{"type":"string"}},"required":["contentType","subject","topic","difficulty","count"]}},"summary":{"type":"string"}},"required":["plan","summary"]}},"generate":{"name":"Generate Content","instruction":"You are an expert educator creating content for children ages 5-14.\n\nFor each item in the content plan from the previous step, generate the content directly.\nThe content must be high-quality, engaging, age-appropriate, and educationally sound.\n\n**For worksheets:**\nGenerate a JSON array of problems. Each problem needs: \"problem\", \"answer\", \"format\" (answer-line | multiple-choice | write-on).\nRequirements:\n- Every answer MUST be correct. Double-check math, facts, spelling.\n- Problems must progress in difficulty within the set.\n- Use at least 2 different problem structures (e.g., equations + word problems for math).\n- Vary phrasing — no two problems should feel copy-pasted.\n- For the target grade level, use appropriate vocabulary and complexity.\n\n**For lessons:**\nGenerate complete HTML + CSS + JS for an interactive lesson.\nRequirements:\n- Must have substantial educational content (not just a shell with navigation).\n- Include interactive elements: quizzes, click-to-reveal, drag-and-drop.\n- Must be visually engaging with colors, icons, and clear typography.\n- Include practice exercises with immediate feedback.\n- Minimum 5 teaching sections plus 2 practice activities.\n\n**For games:**\nGenerate a description and educational requirements only (actual game code is generated\nby the platform's Phaser-based generator).\n\nUse supabase_insert to write each piece of content to the appropriate table.\n\nTable schemas:\n- worksheets: id (auto), title, subject, topic, difficulty, grade_level, num_questions, problems (jsonb), status='published', source='ai-seed'\n- lessons: id (auto), title, subject, difficulty, grade_level, lesson_type, html_code, css_code, js_code, status='published', source='ai-seed'\n- games: id (auto), title, subject, difficulty, grade_level, game_type, html_code, css_code, js_code, status='published', source='ai-seed'\n\nReturn a summary of what was generated and inserted.","skills":["supabase"],"output":{"type":"object","properties":{"generated":{"type":"array","items":{"type":"object","properties":{"contentType":{"type":"string"},"title":{"type":"string"},"subject":{"type":"string"},"id":{"type":"string"},"status":{"type":"string"}}}},"totalGenerated":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["generated","totalGenerated"]}},"validate":{"name":"Validate Content Quality","instruction":"Review the content that was just generated and inserted.\n\nFor each item, use supabase_query to fetch it back and verify:\n\n**Worksheets:**\n- Parse the problems JSON — are all answers correct?\n- Are problems diverse (not repetitive)?\n- Is difficulty appropriate for the stated grade level?\n- Are there the right number of questions?\n\n**Lessons:**\n- Is the HTML substantial (> 500 characters)?\n- Does it contain actual educational content (not just navigation shells)?\n- Are there interactive elements?\n\n**Games:**\n- Does the HTML contain a working game structure?\n\nFlag any issues. If critical issues are found (wrong answers, empty content),\nuse supabase_update to set status='draft' and add a note.\n\nReturn a quality report.","skills":["supabase"],"output":{"type":"object","properties":{"totalReviewed":{"type":"number"},"passed":{"type":"number"},"flagged":{"type":"number"},"issues":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"issue":{"type":"string"},"severity":{"type":"string"},"action":{"type":"string"}}}},"summary":{"type":"string"}},"required":["totalReviewed","passed","flagged","summary"]}},"report":{"name":"Generate Report","instruction":"Compile a final report of the content generation run.\n\nInclude:\n- Total content generated by type and subject\n- Quality validation results\n- Any issues found and actions taken\n- Recommendations for next content generation run (what gaps remain)\n\nFormat as a clean markdown summary that a solo operator can scan in 30 seconds.","skills":[],"output":{"type":"object","properties":{"markdown":{"type":"string"},"nextPriorities":{"type":"array","items":{"type":"string"}}},"required":["markdown"]}}},"edges":[{"from":"audit","to":"generate"},{"from":"generate","to":"validate"},{"from":"validate","to":"report"}],"skills":{}};
|
|
@@ -179,6 +179,44 @@ nodes:
|
|
|
179
179
|
skills:
|
|
180
180
|
- linear
|
|
181
181
|
- github
|
|
182
|
+
output:
|
|
183
|
+
type: object
|
|
184
|
+
properties:
|
|
185
|
+
issueIdentifier:
|
|
186
|
+
type: string
|
|
187
|
+
description: Primary issue identifier (e.g. OFF-1234). This is the first novel issue created — used by downstream nodes for branch naming and PR linking.
|
|
188
|
+
issueTitle:
|
|
189
|
+
type: string
|
|
190
|
+
description: Title of the primary issue created
|
|
191
|
+
issueUrl:
|
|
192
|
+
type: string
|
|
193
|
+
description: URL of the primary issue created
|
|
194
|
+
issues:
|
|
195
|
+
type: array
|
|
196
|
+
description: All issues created or updated during this step
|
|
197
|
+
items:
|
|
198
|
+
type: object
|
|
199
|
+
properties:
|
|
200
|
+
identifier:
|
|
201
|
+
type: string
|
|
202
|
+
title:
|
|
203
|
+
type: string
|
|
204
|
+
url:
|
|
205
|
+
type: string
|
|
206
|
+
action:
|
|
207
|
+
type: string
|
|
208
|
+
enum:
|
|
209
|
+
- created
|
|
210
|
+
- updated
|
|
211
|
+
- reopened
|
|
212
|
+
required:
|
|
213
|
+
- identifier
|
|
214
|
+
- action
|
|
215
|
+
required:
|
|
216
|
+
- issueIdentifier
|
|
217
|
+
- issueTitle
|
|
218
|
+
- issueUrl
|
|
219
|
+
- issues
|
|
182
220
|
skip:
|
|
183
221
|
name: Skip — All Duplicates or Low Priority
|
|
184
222
|
instruction: >-
|
|
@@ -205,13 +243,17 @@ nodes:
|
|
|
205
243
|
implement:
|
|
206
244
|
name: Implement Fix
|
|
207
245
|
instruction: |-
|
|
208
|
-
Implement the fix identified during investigation
|
|
246
|
+
Implement the fix identified during investigation. The issue created in the previous step is in your context — use its identifier for branch naming and commit messages.
|
|
209
247
|
|
|
210
248
|
1. Create a feature branch from the base branch (check context for baseBranch, default "main").
|
|
249
|
+
**Branch name MUST include the issue identifier** (e.g. context.create_issue.issueIdentifier).
|
|
250
|
+
Format: lowercase the identifier and use it as a prefix, e.g. `off-1234-fix-null-check`.
|
|
251
|
+
If the repository uses a different branch convention (check context.branchPattern), follow that instead.
|
|
211
252
|
2. Read the relevant source files to understand the current code.
|
|
212
253
|
3. Make the necessary code changes — fix the bug, nothing more.
|
|
213
254
|
4. Run any existing tests if available to verify the fix doesn't break anything.
|
|
214
|
-
5. Stage and commit with a clear commit message
|
|
255
|
+
5. Stage and commit with a clear commit message. Start the message with the issue identifier in brackets,
|
|
256
|
+
e.g. `[OFF-1234] fix: guard null projectId before database lookup`.
|
|
215
257
|
|
|
216
258
|
Keep changes minimal and focused. Do not refactor surrounding code or add unrelated improvements.
|
|
217
259
|
|
|
@@ -221,15 +263,17 @@ nodes:
|
|
|
221
263
|
create_pr:
|
|
222
264
|
name: Open Pull Request
|
|
223
265
|
instruction: >-
|
|
224
|
-
Open a pull request for the fix
|
|
266
|
+
Open a pull request for the fix. The issue identifier and URL are in your context from the create_issue step.
|
|
225
267
|
|
|
226
268
|
|
|
227
269
|
1. Push the branch to the remote.
|
|
228
270
|
|
|
229
|
-
2. Create a PR with a
|
|
230
|
-
|
|
271
|
+
2. Create a PR with a title that starts with the issue identifier in brackets.
|
|
272
|
+
Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.
|
|
231
273
|
|
|
232
|
-
3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue
|
|
274
|
+
3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue
|
|
275
|
+
(context.create_issue.issueUrl). If the investigation found a Sentry issue, include "Fixes {sentryShortId}"
|
|
276
|
+
in the PR body so Sentry auto-resolves on merge.
|
|
233
277
|
|
|
234
278
|
4. Add appropriate labels if available.
|
|
235
279
|
|