@sweny-ai/core 0.1.79 → 0.1.81
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/claude.d.ts +9 -0
- package/dist/claude.js +36 -0
- package/dist/executor.js +6 -1
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- package/dist/skills/github.js +17 -0
- package/dist/skills/index.d.ts +22 -1
- package/dist/skills/index.js +91 -0
- package/dist/skills/linear.js +20 -0
- package/dist/types.d.ts +19 -0
- package/dist/verify.d.ts +5 -4
- package/dist/verify.js +14 -55
- package/dist/workflows/browser.js +1 -1
- package/dist/workflows/triage.yml +8 -0
- package/package.json +1 -1
package/dist/claude.d.ts
CHANGED
|
@@ -74,3 +74,12 @@ export declare class ClaudeClient implements Claude {
|
|
|
74
74
|
* `"42"`). Preserving the string is safer than guessing.
|
|
75
75
|
*/
|
|
76
76
|
export declare function parseToolResultContent(content: unknown): unknown;
|
|
77
|
+
/**
|
|
78
|
+
* Produce a short, single-line description of a tool-error payload suitable
|
|
79
|
+
* for the CI log stream. The full parsed value stays on the ToolCall for
|
|
80
|
+
* verify and downstream tooling — this is only for inline observability.
|
|
81
|
+
*
|
|
82
|
+
* Collapses newlines, trims whitespace, and caps to 300 chars so a huge
|
|
83
|
+
* API response body doesn't flood the log.
|
|
84
|
+
*/
|
|
85
|
+
export declare function summarizeToolError(parsed: unknown): string;
|
package/dist/claude.js
CHANGED
|
@@ -157,6 +157,15 @@ export class ClaudeClient {
|
|
|
157
157
|
call.status = isError ? "error" : "success";
|
|
158
158
|
const parsed = parseToolResultContent(block.content);
|
|
159
159
|
call.output = isError ? { error: parsed } : parsed;
|
|
160
|
+
// Surface tool errors in the CI log stream at warn level so
|
|
161
|
+
// postmortems don't have to reconstruct them from verify-time
|
|
162
|
+
// tool-call summaries. Without this, "verify failed: tool X
|
|
163
|
+
// did not succeed" never answers the WHY question, because
|
|
164
|
+
// the error body is buried in the per-call output captured
|
|
165
|
+
// only for in-memory verify evaluation.
|
|
166
|
+
if (isError) {
|
|
167
|
+
this.logger.warn(` tool ${call.tool} failed: ${summarizeToolError(parsed)}`);
|
|
168
|
+
}
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
}
|
|
@@ -359,6 +368,33 @@ export function parseToolResultContent(content) {
|
|
|
359
368
|
return content;
|
|
360
369
|
}
|
|
361
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Produce a short, single-line description of a tool-error payload suitable
|
|
373
|
+
* for the CI log stream. The full parsed value stays on the ToolCall for
|
|
374
|
+
* verify and downstream tooling — this is only for inline observability.
|
|
375
|
+
*
|
|
376
|
+
* Collapses newlines, trims whitespace, and caps to 300 chars so a huge
|
|
377
|
+
* API response body doesn't flood the log.
|
|
378
|
+
*/
|
|
379
|
+
export function summarizeToolError(parsed) {
|
|
380
|
+
let raw;
|
|
381
|
+
if (typeof parsed === "string") {
|
|
382
|
+
raw = parsed;
|
|
383
|
+
}
|
|
384
|
+
else if (parsed && typeof parsed === "object") {
|
|
385
|
+
try {
|
|
386
|
+
raw = JSON.stringify(parsed);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
raw = String(parsed);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
raw = String(parsed);
|
|
394
|
+
}
|
|
395
|
+
const collapsed = raw.replace(/\s+/g, " ").trim();
|
|
396
|
+
return collapsed.length > 300 ? collapsed.slice(0, 297) + "..." : collapsed;
|
|
397
|
+
}
|
|
362
398
|
// ─── JSON Schema → Zod conversion ───────────────────────────────
|
|
363
399
|
/**
|
|
364
400
|
* Convert a JSON Schema object to a Zod raw shape for the agent SDK.
|
package/dist/executor.js
CHANGED
|
@@ -17,6 +17,7 @@ import { resolveSources } from "./source-resolver.js";
|
|
|
17
17
|
import { evaluateVerify } from "./verify.js";
|
|
18
18
|
import { evaluateRequires } from "./requires.js";
|
|
19
19
|
import { buildRetryPreamble } from "./retry.js";
|
|
20
|
+
import { buildToolAliases } from "./skills/index.js";
|
|
20
21
|
/**
|
|
21
22
|
* Execute a workflow from entry to completion.
|
|
22
23
|
*
|
|
@@ -37,6 +38,10 @@ export async function execute(workflow, input, options) {
|
|
|
37
38
|
const nodeRunCounts = new Map(); // node → times executed
|
|
38
39
|
const trace = { steps: [], edges: [], sources: {} };
|
|
39
40
|
validate(workflow, skills);
|
|
41
|
+
// Build a verify-time alias table from the loaded skills. Each skill owns
|
|
42
|
+
// its own mapping between skill-tool names and equivalent MCP names. Core
|
|
43
|
+
// stays vendor-neutral — this call just unions what the skills declare.
|
|
44
|
+
const toolAliases = buildToolAliases(skills.values(), logger);
|
|
40
45
|
// ── Source resolution phase ─────────────────────────────────────
|
|
41
46
|
// Resolve all Source values (node instructions + rules/context at every
|
|
42
47
|
// level) into plain text before the main execution loop. This eagerly
|
|
@@ -159,7 +164,7 @@ export async function execute(workflow, input, options) {
|
|
|
159
164
|
// Retry only triggers on verify failure — bail on tool/API errors.
|
|
160
165
|
if (result.status !== "success")
|
|
161
166
|
break;
|
|
162
|
-
const verifyError = evaluateVerify(node.verify, result);
|
|
167
|
+
const verifyError = evaluateVerify(node.verify, result, toolAliases);
|
|
163
168
|
if (!verifyError)
|
|
164
169
|
break;
|
|
165
170
|
// Apply verify failure to the result.
|
package/dist/schema.d.ts
CHANGED
|
@@ -236,6 +236,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
|
|
|
236
236
|
command?: string | undefined;
|
|
237
237
|
args?: string[] | undefined;
|
|
238
238
|
}>>;
|
|
239
|
+
mcpAliases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
|
|
239
240
|
}, "strip", z.ZodTypeAny, {
|
|
240
241
|
id: string;
|
|
241
242
|
name: string;
|
|
@@ -260,6 +261,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
|
|
|
260
261
|
command?: string | undefined;
|
|
261
262
|
args?: string[] | undefined;
|
|
262
263
|
} | undefined;
|
|
264
|
+
mcpAliases?: Record<string, string[]> | undefined;
|
|
263
265
|
}, {
|
|
264
266
|
id: string;
|
|
265
267
|
name: string;
|
|
@@ -284,6 +286,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
|
|
|
284
286
|
description: string;
|
|
285
287
|
input_schema: Record<string, unknown>;
|
|
286
288
|
}[] | undefined;
|
|
289
|
+
mcpAliases?: Record<string, string[]> | undefined;
|
|
287
290
|
}>, {
|
|
288
291
|
id: string;
|
|
289
292
|
name: string;
|
|
@@ -308,6 +311,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
|
|
|
308
311
|
command?: string | undefined;
|
|
309
312
|
args?: string[] | undefined;
|
|
310
313
|
} | undefined;
|
|
314
|
+
mcpAliases?: Record<string, string[]> | undefined;
|
|
311
315
|
}, {
|
|
312
316
|
id: string;
|
|
313
317
|
name: string;
|
|
@@ -332,6 +336,7 @@ export declare const skillZ: z.ZodEffects<z.ZodObject<{
|
|
|
332
336
|
description: string;
|
|
333
337
|
input_schema: Record<string, unknown>;
|
|
334
338
|
}[] | undefined;
|
|
339
|
+
mcpAliases?: Record<string, string[]> | undefined;
|
|
335
340
|
}>;
|
|
336
341
|
/**
|
|
337
342
|
* NodeSources: either an array of Sources (additive — inherits cascade)
|
package/dist/schema.js
CHANGED
|
@@ -61,6 +61,7 @@ export const skillZ = z
|
|
|
61
61
|
tools: z.array(toolZ).default([]),
|
|
62
62
|
instruction: z.string().optional(),
|
|
63
63
|
mcp: mcpServerConfigZ.optional(),
|
|
64
|
+
mcpAliases: z.record(z.array(z.string().min(1)).min(1)).optional(),
|
|
64
65
|
})
|
|
65
66
|
.refine((s) => s.tools.length > 0 || s.instruction || s.mcp, {
|
|
66
67
|
message: "Skill must provide at least one of: tools, instruction, or mcp",
|
package/dist/skills/github.js
CHANGED
|
@@ -183,4 +183,21 @@ export const github = {
|
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
185
|
],
|
|
186
|
+
// Equivalent tool names on GitHub's official MCP server
|
|
187
|
+
// (github.com/github/github-mcp-server). Aliases declared for every
|
|
188
|
+
// GitHub MCP name that does NOT also exist on Linear's MCP. The
|
|
189
|
+
// executor's buildToolAliases already drops any name claimed by more
|
|
190
|
+
// than one loaded skill, so these declarations are safe even if a
|
|
191
|
+
// future provider starts using the same name — declare what this
|
|
192
|
+
// skill knows, let the runtime resolve conflicts.
|
|
193
|
+
//
|
|
194
|
+
// Names that currently exist on both Linear and GitHub MCPs and are
|
|
195
|
+
// therefore intentionally NOT aliased on either side: `get_issue`,
|
|
196
|
+
// `list_issues`.
|
|
197
|
+
mcpAliases: {
|
|
198
|
+
github_create_pr: ["create_pull_request"],
|
|
199
|
+
github_add_comment: ["add_issue_comment"],
|
|
200
|
+
github_create_issue: ["create_issue"],
|
|
201
|
+
github_search_issues: ["search_issues"],
|
|
202
|
+
},
|
|
186
203
|
};
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Custom skill discovery (filesystem-based) lives in `./custom-loader.js`,
|
|
8
8
|
* which is Node-only and re-exported from `@sweny-ai/core` (the Node entry).
|
|
9
9
|
*/
|
|
10
|
-
import type { Skill, SkillCategory } from "../types.js";
|
|
10
|
+
import type { Logger, Skill, SkillCategory } from "../types.js";
|
|
11
11
|
import { github } from "./github.js";
|
|
12
12
|
import { linear } from "./linear.js";
|
|
13
13
|
import { slack } from "./slack.js";
|
|
@@ -86,3 +86,24 @@ export declare function validateWorkflowSkills(workflow: {
|
|
|
86
86
|
skills: string[];
|
|
87
87
|
}>;
|
|
88
88
|
}, available: Map<string, Skill>, inlineSkills?: Record<string, InlineSkillDef>): SkillValidationResult;
|
|
89
|
+
/**
|
|
90
|
+
* Build a symmetric tool-name alias table from the loaded skills.
|
|
91
|
+
*
|
|
92
|
+
* Each skill declares its own `mcpAliases` (canonical name → list of
|
|
93
|
+
* equivalent MCP tool names). This function:
|
|
94
|
+
*
|
|
95
|
+
* 1. Collects every alias pair declared by every loaded skill.
|
|
96
|
+
* 2. Flattens each pair into one equivalence group (symmetric closure).
|
|
97
|
+
* 3. Drops any MCP name claimed by more than one skill (ambiguous).
|
|
98
|
+
*
|
|
99
|
+
* Core stays vendor-neutral: nothing here knows Linear or GitHub by name.
|
|
100
|
+
* Skills own their own naming, and ambiguity is detected at map-build
|
|
101
|
+
* time so a call to one provider's `get_issue` can never spuriously
|
|
102
|
+
* satisfy another provider's `any_tool_called` verify rule.
|
|
103
|
+
*
|
|
104
|
+
* @returns a map from every participating name to the full set of names
|
|
105
|
+
* that count as equivalent to it (including itself). Names that do not
|
|
106
|
+
* participate in any alias are absent — callers should fall back to
|
|
107
|
+
* name-equality when a lookup returns `undefined`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildToolAliases(skills: Iterable<Skill>, logger?: Logger): ReadonlyMap<string, ReadonlySet<string>>;
|
package/dist/skills/index.js
CHANGED
|
@@ -144,3 +144,94 @@ export function validateWorkflowSkills(workflow, available, inlineSkills) {
|
|
|
144
144
|
}
|
|
145
145
|
return { configured, missing, errors, warnings };
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Build a symmetric tool-name alias table from the loaded skills.
|
|
149
|
+
*
|
|
150
|
+
* Each skill declares its own `mcpAliases` (canonical name → list of
|
|
151
|
+
* equivalent MCP tool names). This function:
|
|
152
|
+
*
|
|
153
|
+
* 1. Collects every alias pair declared by every loaded skill.
|
|
154
|
+
* 2. Flattens each pair into one equivalence group (symmetric closure).
|
|
155
|
+
* 3. Drops any MCP name claimed by more than one skill (ambiguous).
|
|
156
|
+
*
|
|
157
|
+
* Core stays vendor-neutral: nothing here knows Linear or GitHub by name.
|
|
158
|
+
* Skills own their own naming, and ambiguity is detected at map-build
|
|
159
|
+
* time so a call to one provider's `get_issue` can never spuriously
|
|
160
|
+
* satisfy another provider's `any_tool_called` verify rule.
|
|
161
|
+
*
|
|
162
|
+
* @returns a map from every participating name to the full set of names
|
|
163
|
+
* that count as equivalent to it (including itself). Names that do not
|
|
164
|
+
* participate in any alias are absent — callers should fall back to
|
|
165
|
+
* name-equality when a lookup returns `undefined`.
|
|
166
|
+
*/
|
|
167
|
+
export function buildToolAliases(skills, logger) {
|
|
168
|
+
// First pass: record which skill(s) claim each MCP alias name.
|
|
169
|
+
const mcpClaims = new Map(); // mcp name → skill ids that declared it
|
|
170
|
+
const pairs = []; // [canonical, mcpAlias]
|
|
171
|
+
for (const skill of skills) {
|
|
172
|
+
if (!skill.mcpAliases)
|
|
173
|
+
continue;
|
|
174
|
+
for (const [canonical, aliases] of Object.entries(skill.mcpAliases)) {
|
|
175
|
+
for (const alias of aliases) {
|
|
176
|
+
if (canonical === alias)
|
|
177
|
+
continue;
|
|
178
|
+
pairs.push([canonical, alias]);
|
|
179
|
+
const claimers = mcpClaims.get(alias);
|
|
180
|
+
if (claimers) {
|
|
181
|
+
if (!claimers.includes(skill.id))
|
|
182
|
+
claimers.push(skill.id);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
mcpClaims.set(alias, [skill.id]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Drop ambiguous MCP names (claimed by >1 skill) and log once.
|
|
191
|
+
const ambiguous = new Set();
|
|
192
|
+
for (const [name, claimers] of mcpClaims) {
|
|
193
|
+
if (claimers.length > 1) {
|
|
194
|
+
ambiguous.add(name);
|
|
195
|
+
logger?.warn(`Tool alias "${name}" is declared by multiple skills (${claimers.join(", ")}); dropping from verify alias table to avoid cross-provider false positives.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Second pass: union-find over the remaining pairs.
|
|
199
|
+
const parent = new Map();
|
|
200
|
+
const find = (x) => {
|
|
201
|
+
let cur = parent.get(x) ?? x;
|
|
202
|
+
while (cur !== (parent.get(cur) ?? cur))
|
|
203
|
+
cur = parent.get(cur) ?? cur;
|
|
204
|
+
parent.set(x, cur);
|
|
205
|
+
return cur;
|
|
206
|
+
};
|
|
207
|
+
const union = (a, b) => {
|
|
208
|
+
const ra = find(a);
|
|
209
|
+
const rb = find(b);
|
|
210
|
+
if (ra !== rb)
|
|
211
|
+
parent.set(ra, rb);
|
|
212
|
+
};
|
|
213
|
+
// `find` registers previously-unseen nodes via `parent.get(x) ?? x` and
|
|
214
|
+
// writes them back, so `union` handles first-contact pairs on its own.
|
|
215
|
+
for (const [canonical, alias] of pairs) {
|
|
216
|
+
if (ambiguous.has(alias))
|
|
217
|
+
continue;
|
|
218
|
+
union(canonical, alias);
|
|
219
|
+
}
|
|
220
|
+
// Build the result: each name → its full equivalence set.
|
|
221
|
+
const groups = new Map();
|
|
222
|
+
for (const name of parent.keys()) {
|
|
223
|
+
const root = find(name);
|
|
224
|
+
let group = groups.get(root);
|
|
225
|
+
if (!group) {
|
|
226
|
+
group = new Set();
|
|
227
|
+
groups.set(root, group);
|
|
228
|
+
}
|
|
229
|
+
group.add(name);
|
|
230
|
+
}
|
|
231
|
+
const result = new Map();
|
|
232
|
+
for (const group of groups.values()) {
|
|
233
|
+
for (const name of group)
|
|
234
|
+
result.set(name, group);
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
package/dist/skills/linear.js
CHANGED
|
@@ -144,4 +144,24 @@ export const linear = {
|
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
146
|
],
|
|
147
|
+
// Equivalent tool names on Linear's official remote MCP server (mcp.linear.app).
|
|
148
|
+
//
|
|
149
|
+
// Linear's MCP uses upsert naming: a single `save_issue` handles both
|
|
150
|
+
// create and update. The executor builds symmetric equivalence groups, so
|
|
151
|
+
// both `linear_create_issue` and `linear_update_issue` end up in one group
|
|
152
|
+
// with `save_issue`. A `save_issue` call satisfies verify rules for either
|
|
153
|
+
// canonical name — verify cannot distinguish create from update when the
|
|
154
|
+
// agent uses the remote MCP. That reflects the provider's reality, not a
|
|
155
|
+
// shortcoming of the alias layer.
|
|
156
|
+
//
|
|
157
|
+
// Names that also exist on GitHub's MCP server (`get_issue`, `list_issues`,
|
|
158
|
+
// `get_team`) are left out. `buildToolAliases` also drops any MCP name
|
|
159
|
+
// claimed by more than one loaded skill as a defensive backstop.
|
|
160
|
+
mcpAliases: {
|
|
161
|
+
linear_create_issue: ["save_issue"],
|
|
162
|
+
linear_update_issue: ["save_issue"],
|
|
163
|
+
linear_search_issues: ["list_issues"],
|
|
164
|
+
linear_add_comment: ["save_comment"],
|
|
165
|
+
linear_list_teams: ["list_teams"],
|
|
166
|
+
},
|
|
147
167
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export interface Skill {
|
|
|
36
36
|
instruction?: string;
|
|
37
37
|
/** External MCP server definition wired for nodes referencing this skill. */
|
|
38
38
|
mcp?: McpServerConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Tool-name aliases recognized by `verify`.
|
|
41
|
+
*
|
|
42
|
+
* When a workflow references `any_tool_called: [linear_create_issue]` and
|
|
43
|
+
* the agent instead calls the equivalent MCP tool exposed by this skill's
|
|
44
|
+
* MCP server (e.g. `save_issue` on Linear's remote MCP), the two names
|
|
45
|
+
* should count as equivalent. Each skill owns the mapping for its own
|
|
46
|
+
* domain — core stays vendor-neutral.
|
|
47
|
+
*
|
|
48
|
+
* Key: a canonical tool name (usually one of this skill's `tools[].name`,
|
|
49
|
+
* but any name the skill wants to equate is valid).
|
|
50
|
+
* Value: list of equivalent names, typically tool names exposed by this
|
|
51
|
+
* skill's external MCP server.
|
|
52
|
+
*
|
|
53
|
+
* Aliases are symmetric: a verify rule naming either side matches a call
|
|
54
|
+
* on either side. Omit names that are ambiguous across providers
|
|
55
|
+
* (e.g. `get_issue` is exposed by both Linear and GitHub MCP servers).
|
|
56
|
+
*/
|
|
57
|
+
mcpAliases?: Record<string, string[]>;
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* Per-node rules or context.
|
package/dist/verify.d.ts
CHANGED
|
@@ -9,9 +9,10 @@ export type Resolution = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function resolvePath(data: unknown, path: string): Resolution;
|
|
11
11
|
export declare function isErrorOutput(output: unknown): boolean;
|
|
12
|
-
export
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function
|
|
12
|
+
export type ToolAliases = ReadonlyMap<string, ReadonlySet<string>>;
|
|
13
|
+
export declare function checkAnyToolCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
14
|
+
export declare function checkAllToolsCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
15
|
+
export declare function checkNoToolCalled(forbidden: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
|
|
15
16
|
export declare function checkOutputRequired(paths: string[], data: unknown): string | null;
|
|
16
17
|
export declare function checkOutputMatches(matches: OutputMatch[], data: unknown): string | null;
|
|
17
18
|
/**
|
|
@@ -21,4 +22,4 @@ export declare function checkOutputMatches(matches: OutputMatch[], data: unknown
|
|
|
21
22
|
* Deterministic and side-effect free — the executor calls this synchronously
|
|
22
23
|
* after the LLM finishes.
|
|
23
24
|
*/
|
|
24
|
-
export declare function evaluateVerify(verify: NodeVerify | undefined, result: NodeResult): string | null;
|
|
25
|
+
export declare function evaluateVerify(verify: NodeVerify | undefined, result: NodeResult, aliases?: ToolAliases): string | null;
|
package/dist/verify.js
CHANGED
|
@@ -105,73 +105,32 @@ function succeededTools(toolCalls) {
|
|
|
105
105
|
function calledList(toolCalls) {
|
|
106
106
|
return toolCalls.map((c) => c.tool).join(", ") || "none";
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// When an external MCP server is wired for the same provider a skill covers
|
|
111
|
-
// (e.g. the Linear HTTP MCP server alongside the `linear` skill), both sets of
|
|
112
|
-
// tools are presented to the agent and the agent may pick either. The calls
|
|
113
|
-
// are functionally equivalent — same backend, same effect — but the names
|
|
114
|
-
// differ, so a verify rule that lists one will fail when the agent picked the
|
|
115
|
-
// other. These aliases let either call satisfy the rule.
|
|
116
|
-
//
|
|
117
|
-
// Only unambiguous MCP tool names are aliased. Names that collide across
|
|
118
|
-
// providers (e.g. `get_issue` exists on both Linear and GitHub MCP servers)
|
|
119
|
-
// are deliberately omitted so a call to one provider cannot spuriously
|
|
120
|
-
// satisfy a rule about the other.
|
|
121
|
-
const TOOL_ALIAS_GROUPS = [
|
|
122
|
-
// Linear (official remote MCP at mcp.linear.app — upsert naming)
|
|
123
|
-
["linear_search_issues", "list_issues"],
|
|
124
|
-
["linear_create_issue", "save_issue"],
|
|
125
|
-
["linear_update_issue", "save_issue"],
|
|
126
|
-
["linear_add_comment", "save_comment"],
|
|
127
|
-
["linear_list_teams", "list_teams"],
|
|
128
|
-
// GitHub (github.com/github/github-mcp-server)
|
|
129
|
-
["github_create_issue", "create_issue"],
|
|
130
|
-
["github_search_issues", "search_issues"],
|
|
131
|
-
["github_add_comment", "add_issue_comment"],
|
|
132
|
-
["github_create_pr", "create_pull_request"],
|
|
133
|
-
];
|
|
134
|
-
const TOOL_ALIASES = (() => {
|
|
135
|
-
const map = new Map();
|
|
136
|
-
for (const group of TOOL_ALIAS_GROUPS) {
|
|
137
|
-
for (const name of group) {
|
|
138
|
-
let set = map.get(name);
|
|
139
|
-
if (!set) {
|
|
140
|
-
set = new Set();
|
|
141
|
-
map.set(name, set);
|
|
142
|
-
}
|
|
143
|
-
for (const other of group)
|
|
144
|
-
set.add(other);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return map;
|
|
148
|
-
})();
|
|
149
|
-
function expandAliases(name) {
|
|
150
|
-
return TOOL_ALIASES.get(name) ?? new Set([name]);
|
|
108
|
+
function expandAliases(name, aliases) {
|
|
109
|
+
return aliases?.get(name) ?? new Set([name]);
|
|
151
110
|
}
|
|
152
|
-
function anyAliasIn(name, set) {
|
|
153
|
-
for (const alias of expandAliases(name)) {
|
|
111
|
+
function anyAliasIn(name, set, aliases) {
|
|
112
|
+
for (const alias of expandAliases(name, aliases)) {
|
|
154
113
|
if (set.has(alias))
|
|
155
114
|
return true;
|
|
156
115
|
}
|
|
157
116
|
return false;
|
|
158
117
|
}
|
|
159
|
-
export function checkAnyToolCalled(required, toolCalls) {
|
|
118
|
+
export function checkAnyToolCalled(required, toolCalls, aliases) {
|
|
160
119
|
const succeeded = succeededTools(toolCalls);
|
|
161
|
-
if (required.some((t) => anyAliasIn(t, succeeded)))
|
|
120
|
+
if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
|
|
162
121
|
return null;
|
|
163
122
|
return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
|
|
164
123
|
}
|
|
165
|
-
export function checkAllToolsCalled(required, toolCalls) {
|
|
124
|
+
export function checkAllToolsCalled(required, toolCalls, aliases) {
|
|
166
125
|
const succeeded = succeededTools(toolCalls);
|
|
167
|
-
const missing = required.filter((t) => !anyAliasIn(t, succeeded));
|
|
126
|
+
const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
|
|
168
127
|
if (missing.length === 0)
|
|
169
128
|
return null;
|
|
170
129
|
return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
171
130
|
}
|
|
172
|
-
export function checkNoToolCalled(forbidden, toolCalls) {
|
|
131
|
+
export function checkNoToolCalled(forbidden, toolCalls, aliases) {
|
|
173
132
|
const calledNames = new Set(toolCalls.map((c) => c.tool));
|
|
174
|
-
const violated = forbidden.filter((t) => anyAliasIn(t, calledNames));
|
|
133
|
+
const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
|
|
175
134
|
if (violated.length === 0)
|
|
176
135
|
return null;
|
|
177
136
|
return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
|
|
@@ -363,22 +322,22 @@ function describeOperator(c) {
|
|
|
363
322
|
* Deterministic and side-effect free — the executor calls this synchronously
|
|
364
323
|
* after the LLM finishes.
|
|
365
324
|
*/
|
|
366
|
-
export function evaluateVerify(verify, result) {
|
|
325
|
+
export function evaluateVerify(verify, result, aliases) {
|
|
367
326
|
if (!verify)
|
|
368
327
|
return null;
|
|
369
328
|
const failures = [];
|
|
370
329
|
if (verify.any_tool_called && verify.any_tool_called.length > 0) {
|
|
371
|
-
const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls);
|
|
330
|
+
const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls, aliases);
|
|
372
331
|
if (e)
|
|
373
332
|
failures.push(e);
|
|
374
333
|
}
|
|
375
334
|
if (verify.all_tools_called && verify.all_tools_called.length > 0) {
|
|
376
|
-
const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls);
|
|
335
|
+
const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls, aliases);
|
|
377
336
|
if (e)
|
|
378
337
|
failures.push(e);
|
|
379
338
|
}
|
|
380
339
|
if (verify.no_tool_called && verify.no_tool_called.length > 0) {
|
|
381
|
-
const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls);
|
|
340
|
+
const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls, aliases);
|
|
382
341
|
if (e)
|
|
383
342
|
failures.push(e);
|
|
384
343
|
}
|
|
@@ -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 — you MUST call tools for this step):** 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. Do NOT skip these searches even if the gather context already mentions existing issues or branches — you must verify independently.\n4a. **Search broadly — THIS REQUIRES TOOL CALLS.** You MUST call `linear_search_issues` (or `github_search_issues`) with 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. If you skip this search, the verify check will fail.\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.\n\n**IMPORTANT — scope boundaries for this node:**\n- DO NOT create issues (Linear, GitHub, or any tracker). A downstream `create_issue` node handles that.\n- DO NOT create branches, commits, or pull requests. A downstream `implement` node handles that.\n- DO NOT call any write tools (`linear_create_issue`, `github_create_issue`, `github_create_pr`, `linear_add_comment`, `github_add_comment`, etc.). Downstream nodes handle all writes — issue creation, +1 comments, and PRs.\n- Your ONLY job is read, search, classify, and output. If you create an issue or add a comment here, the downstream verify check will fail or duplicate work.","skills":["github","linear"],"verify":{"any_tool_called":["linear_search_issues","github_search_issues"]},"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. **First, check if a prior triage run already created an issue for this exact bug.** Call `linear_search_issues` (or `github_search_issues`) with the error message or root cause. If a matching issue already exists with the same root cause:\n - Treat it as a duplicate: DO NOT create a new issue.\n - Populate `issueIdentifier`, `issueTitle`, and `issueUrl` from the existing issue.\n - Add a \"+1\" comment if appropriate (using `linear_add_comment` or `github_add_comment`).\n - Set the `action` to `updated` in the `issues` array.\n - This prevents duplicate tickets when the same alert fires multiple times.\n\n2. If no matching issue exists, create a new one with a clear, actionable title by invoking `linear_create_issue` or `github_create_issue`. You MUST actually call the tool — do NOT synthesize an identifier from an alert ID, commit SHA, or your own imagination. If the tool errors, stop and report the failure; do not invent a fallback identifier. **Use the exact tool names above** — do NOT use generic alternatives like `create_issue` or `create_pull_request` from ToolSearch. The verify check requires the specific tool names listed here.\n3. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n4. Add appropriate labels (bug, severity level, affected service).\n5. Link to relevant commits, PRs, or existing issues.\n6. Copy the identifier returned by the tool verbatim into the `issueIdentifier` output field. The identifier must match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`) — do not prefix it with anything else.\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.\n\n**IMPORTANT — scope boundaries for this node:**\n- DO NOT create branches, commits, or pull requests. The `implement` and `create_pr` nodes handle that.\n- DO NOT call `create_pull_request`, `github_create_pr`, or any PR/branch tools.\n- Your job is issue creation and duplicate management ONLY.","skills":["linear","github"],"verify":{"any_tool_called":["linear_create_issue","github_create_issue","linear_search_issues","github_search_issues","linear_add_comment","github_add_comment"]},"output":{"type":"object","properties":{"issueIdentifier":{"type":"string","pattern":"^[A-Z][A-Z0-9]*-\\d+$","description":"Primary issue identifier (e.g. OFF-1234). MUST be the identifier returned by `linear_create_issue` / `github_create_issue` — do NOT invent a value from a Sentry short-ID or commit SHA. Used by downstream nodes for branch naming and PR linking."},"issueTitle":{"type":"string","description":"Title of the primary issue created"},"issueUrl":{"type":"string","pattern":"^https?://","description":"URL of the primary issue created, as returned by the create-issue tool"},"issues":{"type":"array","minItems":1,"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\n0. **Precondition — verify context.create_issue.issueIdentifier is well-formed.**\n The identifier MUST match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`). If it is\n missing, empty, or matches a Sentry short-ID / slug / any other shape, STOP\n immediately — do NOT invent a branch name from the Sentry event, commit SHA,\n or alert payload. Return a failed status with a clear message instead; the\n workflow will route to notify and a human will pick it up.\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n **Branch name MUST include the issue identifier** exactly as provided in\n `context.create_issue.issueIdentifier`. Format: lowercase the identifier and use\n it as a prefix, e.g. `off-1234-fix-null-check`. If the repository uses a different\n branch convention (check context.branchPattern or the rules/CLAUDE.md for a\n regex), follow that — but the identifier piece MUST still come from\n `context.create_issue.issueIdentifier`, never from a Sentry short-ID.\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 the PR using the `github_create_pr` tool. **Use this exact tool name** — do NOT use `create_pull_request` or any other variant from ToolSearch. The verify check requires `github_create_pr` specifically.\n3. PR title MUST start with the issue identifier in brackets. Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.\n4. 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.\n5. 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"],"verify":{"any_tool_called":["github_create_pr"]}},"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 — you MUST call tools for this step):** 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. Do NOT skip these searches even if the gather context already mentions existing issues or branches — you must verify independently.\n4a. **Search broadly — THIS REQUIRES TOOL CALLS.** You MUST call `linear_search_issues` (or `github_search_issues`) with 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. If you skip this search, the verify check will fail.\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.\n\n**IMPORTANT — scope boundaries for this node:**\n- DO NOT create issues (Linear, GitHub, or any tracker). A downstream `create_issue` node handles that.\n- DO NOT create branches, commits, or pull requests. A downstream `implement` node handles that.\n- DO NOT call any write tools (`linear_create_issue`, `github_create_issue`, `github_create_pr`, `linear_add_comment`, `github_add_comment`, etc.). Downstream nodes handle all writes — issue creation, +1 comments, and PRs.\n- Your ONLY job is read, search, classify, and output. If you create an issue or add a comment here, the downstream verify check will fail or duplicate work.","skills":["github","linear"],"verify":{"any_tool_called":["linear_search_issues","github_search_issues"]},"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. **First, check if a prior triage run already created an issue for this exact bug.** Call `linear_search_issues` (or `github_search_issues`) with the error message or root cause. If a matching issue already exists with the same root cause:\n - Treat it as a duplicate: DO NOT create a new issue.\n - Populate `issueIdentifier`, `issueTitle`, and `issueUrl` from the existing issue.\n - Add a \"+1\" comment if appropriate (using `linear_add_comment` or `github_add_comment`).\n - Set the `action` to `updated` in the `issues` array.\n - This prevents duplicate tickets when the same alert fires multiple times.\n\n2. If no matching issue exists, create a new one with a clear, actionable title by invoking `linear_create_issue` or `github_create_issue`. You MUST actually call the tool — do NOT synthesize an identifier from an alert ID, commit SHA, or your own imagination. If the tool errors, stop and report the failure; do not invent a fallback identifier. **Use the exact tool names above** — do NOT use generic alternatives like `create_issue` or `create_pull_request` from ToolSearch. The verify check requires the specific tool names listed here.\n3. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n4. Add appropriate labels (bug, severity level, affected service).\n5. Link to relevant commits, PRs, or existing issues.\n6. Copy the identifier returned by the tool verbatim into the `issueIdentifier` output field. The identifier must match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`) — do not prefix it with anything else.\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.\n\n**IMPORTANT — scope boundaries for this node:**\n- DO NOT create branches, commits, or pull requests. The `implement` and `create_pr` nodes handle that.\n- DO NOT call `create_pull_request`, `github_create_pr`, or any PR/branch tools.\n- Your job is issue creation and duplicate management ONLY.","skills":["linear","github"],"verify":{"any_tool_called":["linear_create_issue","github_create_issue","linear_search_issues","github_search_issues","linear_add_comment","github_add_comment"]},"output":{"type":"object","properties":{"issueIdentifier":{"type":"string","pattern":"^[A-Z][A-Z0-9]*-\\d+$","description":"Primary issue identifier (e.g. OFF-1234). MUST be the identifier returned by `linear_create_issue` / `github_create_issue` — do NOT invent a value from a Sentry short-ID or commit SHA. Used by downstream nodes for branch naming and PR linking."},"issueTitle":{"type":"string","description":"Title of the primary issue created"},"issueUrl":{"type":"string","pattern":"^https?://","description":"URL of the primary issue created, as returned by the create-issue tool"},"issues":{"type":"array","minItems":1,"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"]},"retry":{"max":1,"instruction":{"auto":true}}},"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\n0. **Precondition — verify context.create_issue.issueIdentifier is well-formed.**\n The identifier MUST match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`). If it is\n missing, empty, or matches a Sentry short-ID / slug / any other shape, STOP\n immediately — do NOT invent a branch name from the Sentry event, commit SHA,\n or alert payload. Return a failed status with a clear message instead; the\n workflow will route to notify and a human will pick it up.\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n **Branch name MUST include the issue identifier** exactly as provided in\n `context.create_issue.issueIdentifier`. Format: lowercase the identifier and use\n it as a prefix, e.g. `off-1234-fix-null-check`. If the repository uses a different\n branch convention (check context.branchPattern or the rules/CLAUDE.md for a\n regex), follow that — but the identifier piece MUST still come from\n `context.create_issue.issueIdentifier`, never from a Sentry short-ID.\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 the PR using the `github_create_pr` tool. **Use this exact tool name** — do NOT use `create_pull_request` or any other variant from ToolSearch. The verify check requires `github_create_pr` specifically.\n3. PR title MUST start with the issue identifier in brackets. Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.\n4. 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.\n5. 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"],"verify":{"any_tool_called":["github_create_pr"]},"retry":{"max":1,"instruction":{"auto":true}}},"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":{}};
|
|
@@ -289,6 +289,10 @@ nodes:
|
|
|
289
289
|
- issueTitle
|
|
290
290
|
- issueUrl
|
|
291
291
|
- issues
|
|
292
|
+
retry:
|
|
293
|
+
max: 1
|
|
294
|
+
instruction:
|
|
295
|
+
auto: true
|
|
292
296
|
skip:
|
|
293
297
|
name: Skip — All Duplicates or Low Priority
|
|
294
298
|
instruction: >-
|
|
@@ -378,6 +382,10 @@ nodes:
|
|
|
378
382
|
verify:
|
|
379
383
|
any_tool_called:
|
|
380
384
|
- github_create_pr
|
|
385
|
+
retry:
|
|
386
|
+
max: 1
|
|
387
|
+
instruction:
|
|
388
|
+
auto: true
|
|
381
389
|
notify:
|
|
382
390
|
name: Notify Team
|
|
383
391
|
instruction: |-
|