@sweny-ai/core 0.1.210 → 0.1.212
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/main.js +4 -0
- package/dist/cli/packs.d.ts +1 -0
- package/dist/cli/packs.js +113 -44
- package/dist/cli/templates.js +148 -21
- package/dist/cli/workflow-upgrade.d.ts +7 -1
- package/dist/cli/workflow-upgrade.js +28 -2
- package/dist/executor.js +3 -2
- package/dist/journal.d.ts +2 -1
- package/dist/journal.js +4 -3
- package/dist/loader.js +5 -0
- package/dist/migrations.d.ts +33 -2
- package/dist/migrations.js +106 -4
- package/dist/node-policy.d.ts +27 -4
- package/dist/node-policy.js +59 -4
- package/dist/safe-outputs.d.ts +3 -1
- package/dist/safe-outputs.js +22 -4
- package/dist/schema.d.ts +30 -10
- package/dist/schema.js +14 -7
- package/dist/skills/github.d.ts +48 -0
- package/dist/skills/github.js +143 -0
- package/dist/types.d.ts +15 -4
- package/dist/workflow-builder.d.ts +1 -1
- package/dist/workflow-builder.js +11 -4
- package/dist/workflows/browser.js +3 -3
- package/dist/workflows/implement.yml +7 -0
- package/dist/workflows/seed-content.yml +17 -0
- package/dist/workflows/triage.yml +7 -0
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -303,7 +303,7 @@ export const safeOutputDeclarationZ = z
|
|
|
303
303
|
type: z.enum(SAFE_OUTPUT_TYPES),
|
|
304
304
|
via: z.string().min(1).optional(),
|
|
305
305
|
max: z.number().int().min(1).max(SAFE_OUTPUT_MAX_CEILING).optional(),
|
|
306
|
-
target: z.string().min(1).optional(),
|
|
306
|
+
target: z.union([z.string().min(1), z.object({ input: z.string().min(1) }).strict()]).optional(),
|
|
307
307
|
title_prefix: z.string().min(1).max(64).optional(),
|
|
308
308
|
labels: z.array(z.string().min(1)).min(1).optional(),
|
|
309
309
|
expires: z.string().regex(SAFE_OUTPUT_EXPIRES_PATTERN).optional(),
|
|
@@ -1073,9 +1073,16 @@ export const workflowJsonSchema = {
|
|
|
1073
1073
|
description: "Max writes of this type from this node per run. Default 1.",
|
|
1074
1074
|
},
|
|
1075
1075
|
target: {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1076
|
+
description: "Pinned target (GitHub owner/repo, Linear team id), or { input: <name> } to take it from a run input. GitHub default: GITHUB_REPOSITORY. An output that pins number never takes its GitHub repository from the agent.",
|
|
1077
|
+
oneOf: [
|
|
1078
|
+
{ type: "string", minLength: 1 },
|
|
1079
|
+
{
|
|
1080
|
+
type: "object",
|
|
1081
|
+
required: ["input"],
|
|
1082
|
+
additionalProperties: false,
|
|
1083
|
+
properties: { input: { type: "string", minLength: 1 } },
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1079
1086
|
},
|
|
1080
1087
|
title_prefix: {
|
|
1081
1088
|
type: "string",
|
|
@@ -1122,7 +1129,7 @@ export const workflowJsonSchema = {
|
|
|
1122
1129
|
spec_version: {
|
|
1123
1130
|
type: "string",
|
|
1124
1131
|
pattern: "^[1-9][0-9]*$",
|
|
1125
|
-
description: "Workflow spec version as a positive integer string. Absent means 1.
|
|
1132
|
+
description: "Workflow spec version as a positive integer string. Absent means 1. Version 2 is safe by default: a node that declares no permissions and no outputs, in a workflow that declares no permissions, is read-only. Version 1 files are migrated in memory on load, keeping write on such nodes with one warning each; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.",
|
|
1126
1133
|
},
|
|
1127
1134
|
entry: { type: "string", minLength: 1, description: "ID of the entry node" },
|
|
1128
1135
|
rules: {
|
|
@@ -1155,7 +1162,7 @@ export const workflowJsonSchema = {
|
|
|
1155
1162
|
},
|
|
1156
1163
|
permissions: {
|
|
1157
1164
|
$ref: "#/$defs/Permissions",
|
|
1158
|
-
description: "Default and ceiling for every node's permissions. Absent: write.",
|
|
1165
|
+
description: "Default and ceiling for every node's permissions. Absent: read (spec_version 2), write (spec_version 1).",
|
|
1159
1166
|
},
|
|
1160
1167
|
safe_outputs: {
|
|
1161
1168
|
type: "object",
|
|
@@ -1374,7 +1381,7 @@ export const workflowJsonSchema = {
|
|
|
1374
1381
|
},
|
|
1375
1382
|
permissions: {
|
|
1376
1383
|
$ref: "#/$defs/Permissions",
|
|
1377
|
-
description: "What this node's agent may do. Absent: read when the node declares outputs, else the workflow's permissions, else write.",
|
|
1384
|
+
description: "What this node's agent may do. Absent: read when the node declares outputs, else the workflow's permissions, else read (spec_version 2) or write (spec_version 1).",
|
|
1378
1385
|
},
|
|
1379
1386
|
outputs: {
|
|
1380
1387
|
type: "array",
|
package/dist/skills/github.d.ts
CHANGED
|
@@ -13,6 +13,54 @@ export type { BranchPusher } from "../types.js";
|
|
|
13
13
|
* fragment, since the token goes to it.
|
|
14
14
|
*/
|
|
15
15
|
export declare function githubApiBase(apiUrl: string | undefined): string;
|
|
16
|
+
/**
|
|
17
|
+
* Hard caps for `github_get_pr_patches`: the agent names the files, the tool
|
|
18
|
+
* decides how much of them it gets. A reviewer's evidence, not the whole diff.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PR_PATCH_CAPS: {
|
|
21
|
+
readonly maxFiles: 8;
|
|
22
|
+
readonly maxBytesPerFile: 6000;
|
|
23
|
+
readonly maxTotalBytes: 24000;
|
|
24
|
+
};
|
|
25
|
+
export interface PrPatchHunk {
|
|
26
|
+
/** The hunk header alone, e.g. `@@ -10,6 +10,9 @@`. */
|
|
27
|
+
header: string;
|
|
28
|
+
/** New-file line range the hunk covers (`start` = `end` = the old position for a pure deletion). */
|
|
29
|
+
new_start: number;
|
|
30
|
+
new_end: number;
|
|
31
|
+
/** The hunk body (context, `+` and `-` lines), header line included. */
|
|
32
|
+
text: string;
|
|
33
|
+
/** True when the byte cap cut this hunk short. */
|
|
34
|
+
cut?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface PrPatchSelection {
|
|
37
|
+
files: Array<{
|
|
38
|
+
filename: string;
|
|
39
|
+
status: string;
|
|
40
|
+
additions: number;
|
|
41
|
+
deletions: number;
|
|
42
|
+
hunks: PrPatchHunk[];
|
|
43
|
+
/** True when the byte caps dropped or cut any of this file's hunks. */
|
|
44
|
+
truncated: boolean;
|
|
45
|
+
hunks_omitted: number;
|
|
46
|
+
}>;
|
|
47
|
+
omitted: Array<{
|
|
48
|
+
filename: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** True when anything requested was left out or cut. */
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
bytes: number;
|
|
54
|
+
caps: typeof PR_PATCH_CAPS;
|
|
55
|
+
}
|
|
56
|
+
/** Split a GitHub file patch into hunks at each `@@` header. */
|
|
57
|
+
export declare function splitHunks(patch: string): PrPatchHunk[];
|
|
58
|
+
/**
|
|
59
|
+
* Apply {@link PR_PATCH_CAPS} to the requested files, in the order asked.
|
|
60
|
+
* Pure: `found` is the PR's file records by filename (GitHub's `patch` is
|
|
61
|
+
* absent for binary files and for diffs too large for the API).
|
|
62
|
+
*/
|
|
63
|
+
export declare function selectPrPatches(wanted: string[], found: Map<string, Record<string, unknown>>): PrPatchSelection;
|
|
16
64
|
/** The GraphQL endpoint beside the REST base: `/api/graphql` on GHES, `/graphql` on github.com. */
|
|
17
65
|
export declare function githubGraphqlUrl(apiUrl: string | undefined): string;
|
|
18
66
|
/**
|
package/dist/skills/github.js
CHANGED
|
@@ -50,6 +50,113 @@ async function gh(path, ctx, init) {
|
|
|
50
50
|
throw new GitHubApiError(res.status, await res.text());
|
|
51
51
|
return res.json();
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Hard caps for `github_get_pr_patches`: the agent names the files, the tool
|
|
55
|
+
* decides how much of them it gets. A reviewer's evidence, not the whole diff.
|
|
56
|
+
*/
|
|
57
|
+
export const PR_PATCH_CAPS = { maxFiles: 8, maxBytesPerFile: 6000, maxTotalBytes: 24000 };
|
|
58
|
+
/** The PR files endpoint lists at most 3000 files, 100 a page. */
|
|
59
|
+
const PR_FILES_MAX_PAGES = 30;
|
|
60
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
61
|
+
/** Split a GitHub file patch into hunks at each `@@` header. */
|
|
62
|
+
export function splitHunks(patch) {
|
|
63
|
+
const hunks = [];
|
|
64
|
+
let cur = null;
|
|
65
|
+
for (const line of patch.split("\n")) {
|
|
66
|
+
const m = HUNK_HEADER.exec(line);
|
|
67
|
+
if (m) {
|
|
68
|
+
const start = Number(m[3]);
|
|
69
|
+
const len = m[4] === undefined ? 1 : Number(m[4]);
|
|
70
|
+
cur = { header: m[0], new_start: start, new_end: len > 0 ? start + len - 1 : start, text: line };
|
|
71
|
+
hunks.push(cur);
|
|
72
|
+
}
|
|
73
|
+
else if (cur) {
|
|
74
|
+
cur.text += `\n${line}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return hunks;
|
|
78
|
+
}
|
|
79
|
+
const utf8 = new TextEncoder();
|
|
80
|
+
const byteLength = (s) => utf8.encode(s).length;
|
|
81
|
+
/** The longest prefix of `s` that fits in `max` UTF-8 bytes, never splitting a character. */
|
|
82
|
+
function cutToBytes(s, max) {
|
|
83
|
+
let out = "";
|
|
84
|
+
let used = 0;
|
|
85
|
+
for (const ch of s) {
|
|
86
|
+
const n = byteLength(ch);
|
|
87
|
+
if (used + n > max)
|
|
88
|
+
break;
|
|
89
|
+
out += ch;
|
|
90
|
+
used += n;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Apply {@link PR_PATCH_CAPS} to the requested files, in the order asked.
|
|
96
|
+
* Pure: `found` is the PR's file records by filename (GitHub's `patch` is
|
|
97
|
+
* absent for binary files and for diffs too large for the API).
|
|
98
|
+
*/
|
|
99
|
+
export function selectPrPatches(wanted, found) {
|
|
100
|
+
const caps = PR_PATCH_CAPS;
|
|
101
|
+
const out = { files: [], omitted: [], truncated: false, bytes: 0, caps };
|
|
102
|
+
for (const filename of wanted) {
|
|
103
|
+
const f = found.get(filename);
|
|
104
|
+
if (!f) {
|
|
105
|
+
out.omitted.push({ filename, reason: "not a changed file in this pull request" });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (typeof f.patch !== "string" || f.patch.length === 0) {
|
|
109
|
+
out.omitted.push({ filename, reason: "no patch from GitHub (binary file, or a diff too large for the API)" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (out.files.length >= caps.maxFiles) {
|
|
113
|
+
out.omitted.push({ filename, reason: `file cap (${caps.maxFiles} files)` });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const left = caps.maxTotalBytes - out.bytes;
|
|
117
|
+
if (left <= 0) {
|
|
118
|
+
out.omitted.push({ filename, reason: `byte budget spent (${caps.maxTotalBytes} bytes in all)` });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const budget = Math.min(caps.maxBytesPerFile, left);
|
|
122
|
+
const all = splitHunks(f.patch);
|
|
123
|
+
const hunks = [];
|
|
124
|
+
let used = 0;
|
|
125
|
+
for (const h of all) {
|
|
126
|
+
const n = byteLength(h.text);
|
|
127
|
+
if (used + n <= budget) {
|
|
128
|
+
hunks.push(h);
|
|
129
|
+
used += n;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// The first hunk that does not fit: keep its head when nothing of this
|
|
133
|
+
// file fits yet, so a large single-hunk file still shows its start.
|
|
134
|
+
if (hunks.length === 0) {
|
|
135
|
+
const text = cutToBytes(h.text, budget);
|
|
136
|
+
hunks.push({ ...h, text, cut: true });
|
|
137
|
+
used += byteLength(text);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const dropped = all.length - hunks.length;
|
|
142
|
+
const truncated = dropped > 0 || hunks.some((h) => h.cut);
|
|
143
|
+
out.files.push({
|
|
144
|
+
filename,
|
|
145
|
+
status: String(f.status ?? ""),
|
|
146
|
+
additions: Number(f.additions ?? 0),
|
|
147
|
+
deletions: Number(f.deletions ?? 0),
|
|
148
|
+
hunks,
|
|
149
|
+
truncated,
|
|
150
|
+
hunks_omitted: dropped,
|
|
151
|
+
});
|
|
152
|
+
out.bytes += used;
|
|
153
|
+
if (truncated)
|
|
154
|
+
out.truncated = true;
|
|
155
|
+
}
|
|
156
|
+
if (out.omitted.length > 0)
|
|
157
|
+
out.truncated = true;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
53
160
|
/** The GraphQL endpoint beside the REST base: `/api/graphql` on GHES, `/graphql` on github.com. */
|
|
54
161
|
export function githubGraphqlUrl(apiUrl) {
|
|
55
162
|
const base = githubApiBase(apiUrl);
|
|
@@ -700,6 +807,42 @@ export const github = {
|
|
|
700
807
|
};
|
|
701
808
|
},
|
|
702
809
|
},
|
|
810
|
+
{
|
|
811
|
+
name: "github_get_pr_patches",
|
|
812
|
+
access: "read",
|
|
813
|
+
description: "Read the patch hunks of named files in a pull request, under hard caps " +
|
|
814
|
+
`(${PR_PATCH_CAPS.maxFiles} files, ${PR_PATCH_CAPS.maxBytesPerFile} bytes a file, ` +
|
|
815
|
+
`${PR_PATCH_CAPS.maxTotalBytes} bytes in all). ` +
|
|
816
|
+
"Each hunk carries its header (e.g. '@@ -10,6 +10,9 @@') and its new-file line range. " +
|
|
817
|
+
"Everything left out is listed in `omitted` with the reason, and cut files are marked `truncated`. " +
|
|
818
|
+
"Pass the riskiest paths first: the caps keep the order you give.",
|
|
819
|
+
input_schema: {
|
|
820
|
+
type: "object",
|
|
821
|
+
properties: {
|
|
822
|
+
repo: { type: "string", description: "owner/repo" },
|
|
823
|
+
number: { type: "number", description: "Pull request number" },
|
|
824
|
+
paths: {
|
|
825
|
+
type: "array",
|
|
826
|
+
items: { type: "string" },
|
|
827
|
+
description: `Changed file paths to read, most important first (at most ${PR_PATCH_CAPS.maxFiles} are read)`,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
required: ["repo", "number", "paths"],
|
|
831
|
+
},
|
|
832
|
+
handler: async (input, ctx) => {
|
|
833
|
+
const wanted = [...new Set((Array.isArray(input.paths) ? input.paths : []).map(String))];
|
|
834
|
+
const found = new Map();
|
|
835
|
+
for (let page = 1; page <= PR_FILES_MAX_PAGES; page++) {
|
|
836
|
+
const batch = (await gh(`/repos/${input.repo}/pulls/${input.number}/files?per_page=100&page=${page}`, ctx));
|
|
837
|
+
for (const f of batch)
|
|
838
|
+
if (wanted.includes(String(f.filename)))
|
|
839
|
+
found.set(String(f.filename), f);
|
|
840
|
+
if (batch.length < 100 || found.size === wanted.length)
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
return selectPrPatches(wanted, found);
|
|
844
|
+
},
|
|
845
|
+
},
|
|
703
846
|
{
|
|
704
847
|
name: "github_list_dependabot_alerts",
|
|
705
848
|
access: "read",
|
package/dist/types.d.ts
CHANGED
|
@@ -502,7 +502,7 @@ export interface Node {
|
|
|
502
502
|
* only `access: "read"` skill tools, no external skill MCP servers, no
|
|
503
503
|
* shell / file-write / edit / fetch / subagent built-ins. Absent: `read`
|
|
504
504
|
* when the node declares `outputs`, else the workflow's `permissions`,
|
|
505
|
-
* else `write` (
|
|
505
|
+
* else `read` from spec_version 2, `write` in spec_version 1 (#598).
|
|
506
506
|
*/
|
|
507
507
|
permissions?: NodePermissions;
|
|
508
508
|
/**
|
|
@@ -561,8 +561,12 @@ export interface SafeOutputDeclaration {
|
|
|
561
561
|
via?: string;
|
|
562
562
|
/** Max writes of this type from this node per run. Default 1. */
|
|
563
563
|
max?: number;
|
|
564
|
-
/**
|
|
565
|
-
|
|
564
|
+
/**
|
|
565
|
+
* Pinned target (GitHub `owner/repo`, Linear team id), or `{ input: <name> }`
|
|
566
|
+
* to take it from a run input. GitHub default: `GITHUB_REPOSITORY`. An output
|
|
567
|
+
* pinned to an issue (`number`) never takes its GitHub repository from the agent (#598).
|
|
568
|
+
*/
|
|
569
|
+
target?: SafeOutputTarget;
|
|
566
570
|
/** Prepended to issue / PR titles that do not already start with it. */
|
|
567
571
|
title_prefix?: string;
|
|
568
572
|
/** issue / pr: labels always added. label: the only labels the agent may add. */
|
|
@@ -580,6 +584,10 @@ export interface SafeOutputDeclaration {
|
|
|
580
584
|
/** issue_state only: the one change this output may make. Default: either. */
|
|
581
585
|
state?: SafeOutputState;
|
|
582
586
|
}
|
|
587
|
+
/** A pinned target: a literal (`owner/repo`, team id), or the name of a run input that holds one. */
|
|
588
|
+
export type SafeOutputTarget = string | {
|
|
589
|
+
input: string;
|
|
590
|
+
};
|
|
583
591
|
/** A pinned issue / PR: a literal number or identifier, or the name of a run input that holds one. */
|
|
584
592
|
export type SafeOutputPin = string | number | {
|
|
585
593
|
input: string;
|
|
@@ -733,7 +741,10 @@ export interface Workflow {
|
|
|
733
741
|
* See `src/inputs.ts` for the field shape and validation rules.
|
|
734
742
|
*/
|
|
735
743
|
inputs?: import("./inputs.js").WorkflowInputs;
|
|
736
|
-
/**
|
|
744
|
+
/**
|
|
745
|
+
* Default and ceiling for every node's `permissions` (#365). Absent: `read`
|
|
746
|
+
* from spec_version 2, `write` in spec_version 1 (#598).
|
|
747
|
+
*/
|
|
737
748
|
permissions?: NodePermissions;
|
|
738
749
|
/** Workflow-level safe-output policy (#365). */
|
|
739
750
|
safe_outputs?: SafeOutputsPolicy;
|
|
@@ -26,7 +26,7 @@ export declare function buildSystemPrompt(skills: Skill[], existingWorkflow?: Wo
|
|
|
26
26
|
* adds a `summary` key from the text response. We strip `summary`
|
|
27
27
|
* and any other non-workflow keys before parsing.
|
|
28
28
|
*/
|
|
29
|
-
export declare function extractWorkflow(data: Record<string, unknown>): Workflow;
|
|
29
|
+
export declare function extractWorkflow(data: Record<string, unknown>, keep?: Pick<Workflow, "spec_version">): Workflow;
|
|
30
30
|
/**
|
|
31
31
|
* Derive the required environment variables for a workflow from its skills.
|
|
32
32
|
* Deterministic — doesn't depend on Claude generating a variables block.
|
package/dist/workflow-builder.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* description and available skills, and returns a validated Workflow.
|
|
7
7
|
*/
|
|
8
8
|
import { workflowZ, validateWorkflow, workflowJsonSchema } from "./schema.js";
|
|
9
|
+
import { CURRENT_SPEC_VERSION } from "./migrations.js";
|
|
9
10
|
// ─── Instruction quality guidance ────────────────────────────────
|
|
10
11
|
const INSTRUCTION_GUIDANCE = `Each node's \`instruction\` field is a detailed prompt that Claude will execute autonomously.
|
|
11
12
|
Write instructions as if briefing a skilled engineer who has access to the node's tools
|
|
@@ -60,6 +61,7 @@ export function buildSystemPrompt(skills, existingWorkflow) {
|
|
|
60
61
|
"- Use a natural-language `when` only for a fuzzy condition no declared field can express",
|
|
61
62
|
"- Put safety conditions (anything that gates a write, a PR or an issue) in expressions, never in natural language",
|
|
62
63
|
"- Every node must be reachable from the entry node",
|
|
64
|
+
"- Nodes are read-only unless they declare otherwise (spec_version 2). A node that files issues, comments or opens PRs declares `outputs` (comment, issue, pr, label, issue_state) and stays read-only; any other node that must change something declares `permissions: write`, narrowed with `deny` to the built-in tool classes it does not need",
|
|
63
65
|
"- Return ONLY the workflow JSON object — no markdown fences, no explanation",
|
|
64
66
|
];
|
|
65
67
|
if (existingWorkflow) {
|
|
@@ -73,9 +75,14 @@ export function buildSystemPrompt(skills, existingWorkflow) {
|
|
|
73
75
|
* adds a `summary` key from the text response. We strip `summary`
|
|
74
76
|
* and any other non-workflow keys before parsing.
|
|
75
77
|
*/
|
|
76
|
-
export function extractWorkflow(data) {
|
|
77
|
-
// Remove keys added by ClaudeClient that aren't part of the workflow schema
|
|
78
|
-
|
|
78
|
+
export function extractWorkflow(data, keep) {
|
|
79
|
+
// Remove keys added by ClaudeClient that aren't part of the workflow schema.
|
|
80
|
+
// The spec version is ours to set, never the model's (#598): a new workflow is
|
|
81
|
+
// current (read-only by default), a refined one keeps the version it had.
|
|
82
|
+
const { summary: _summary, spec_version: _modelVersion, ...body } = data;
|
|
83
|
+
// `keep` (a refine) holds the version the workflow had, possibly none; a build gets the current one.
|
|
84
|
+
const specVersion = keep ? keep.spec_version : String(CURRENT_SPEC_VERSION);
|
|
85
|
+
const rest = specVersion === undefined ? body : { spec_version: specVersion, ...body };
|
|
79
86
|
// workflowZ.parse throws a ZodError on invalid input
|
|
80
87
|
const parsed = workflowZ.parse(rest);
|
|
81
88
|
const errors = validateWorkflow(parsed);
|
|
@@ -158,5 +165,5 @@ export async function refineWorkflow(workflow, instruction, options) {
|
|
|
158
165
|
if (result.status === "failed") {
|
|
159
166
|
throw new Error(`Claude failed to refine workflow: ${result.data.error ?? "unknown error"}`);
|
|
160
167
|
}
|
|
161
|
-
return extractWorkflow(result.data);
|
|
168
|
+
return extractWorkflow(result.data, { spec_version: workflow.spec_version });
|
|
162
169
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const triageWorkflow = {"id":"triage","name":"Alert Triage","workflow_type":"monitor","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","permissions":"write","safe_outputs":{"allow":["issue","comment","issue_state","pr"],"max":25},"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, but stay in scope: read, search, and summarize. The investigation step depends on complete context, not on fixes.\n\n**Scope boundary (this node is READ-ONLY):**\n- DO NOT create issues, comments, branches, commits, or pull requests, and DO NOT implement fixes. Downstream nodes (`create_issue`, `implement`, `create_pr`) handle ALL writes.\n- Your ONLY job is to collect context and report it, then stop. Doing downstream work here burns your turn budget and duplicates what later nodes will do.","skills":["github","sentry","datadog","betterstack","linear"],"permissions":"read","fail_soft":true},"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 eval 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- `fixable_count`: how many NOVEL findings have `fix_complexity` simple or moderate AND a non-empty `fix_approach`. 0 when none do. This decides whether sweny attempts a fix after filing the issue.\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 eval check will fail or duplicate work.","skills":["github","linear"],"permissions":"read","eval":[{"name":"novelty_search_was_performed","kind":"function","rule":{"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"},"fixable_count":{"type":"integer","minimum":0,"description":"Count of novel findings whose fix_complexity is simple or moderate AND whose fix_approach is a non-empty plan. Routes create_issue to implement.","x-sweny-count-of":{"array":"findings","where":{"is_duplicate":[false],"fix_complexity":["simple","moderate"]},"non_empty":["fix_approach"]}},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","fixable_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**How writes work here.** If you have the `emit_output` tool, you cannot write to the tracker directly: request each new issue and each comment with `emit_output`, and sweny files them after this step, within its limits. A requested issue has no identifier yet; sweny records it for the next steps. Without `emit_output`, use the tracker's own tools (for example `linear_create_issue`, `github_create_issue`, `linear_add_comment`, `github_add_comment`, or the configured tracker's MCP tools) and copy the identifier they return.\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.\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, file a new one with a clear, actionable title (type `issue`). Do NOT synthesize an identifier from an alert ID, commit SHA, or your own imagination: a new issue's identifier comes only from the tracker. Set `dedupe_key` to a short stable fingerprint of the root cause, so two findings with the same cause are filed once. Set the `action` to `requested` (or `created` when you filed it with a tracker tool and have its identifier).\n3. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n4. Apply the `sweny`, `agent`, and `triage` labels. For Linear: call `linear_list_labels` to find the IDs for labels named `sweny`, `agent`, and `triage`, and pass those IDs as the labels (skip any that don't exist in the team; don't invent IDs). For Linear, the target is the team ID (`linear_list_teams`). For GitHub: labels `[\"sweny\", \"agent\", \"triage\"]`.\n5. Link to relevant commits, PRs, or existing issues.\n6. When you know the identifier (an existing issue, or one a tracker tool returned), copy it verbatim into `issueIdentifier`. It must match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`); do not prefix it with anything else. Leave `issueIdentifier` unset for an issue you only requested.\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 (type `comment`, `number` = the issue ID or number): \"+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 or done: reopen it (type `issue_state`, `state` = `reopen`, `number` = the issue ID or number) when you have `emit_output`; with a tracker tool instead, use one that can reopen. If you can do neither, say in the comment that it is closed and was seen again.\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 issues you requested, created, or updated.\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- Your job is issue creation and duplicate management ONLY.","skills":["linear","github"],"outputs":[{"type":"issue","max":10},{"type":"comment","max":10},{"type":"issue_state","state":"reopen","max":10}],"tools":{"deny":["github_create_pr"]},"eval":[{"name":"issue_tracker_tool_was_called","kind":"function","rule":{"any_tool_called":["emit_output","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) when known in this step: an existing issue the duplicate check matched, or one a tracker tool returned. Unset for an issue only requested through emit_output; sweny records its identifier in safe_outputs. Never invented from a Sentry short-ID or commit SHA."},"issueTitle":{"type":"string","description":"Title of the primary issue"},"issueUrl":{"type":"string","pattern":"^https?://","description":"URL of the primary issue, when known in this step"},"issues":{"type":"array","minItems":1,"description":"All issues requested, created, or updated during this step","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"action":{"type":"string","enum":["requested","created","updated","reopened"]}},"required":["action"]}}},"required":["issueTitle","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 (with `emit_output` type `comment` when you have it, `number` = the issue ID or number): \"+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 or done: reopen it (`emit_output` type `issue_state`, `state` = `reopen`, `number` = the issue ID or number) when you have `emit_output`; with a tracker tool instead, use one that can reopen. If you can do neither, say in the comment that it is closed and was seen again.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"],"outputs":[{"type":"comment","max":10},{"type":"issue_state","state":"reopen","max":10}]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation, plus the tests that pin\nit as a regression guard. The issue from the previous step is in your\ncontext. Use its identifier for branch naming and commit messages.\n\n**The issue identifier** (ISSUE_ID below): the `ref` of the `issue` entry\nwith status `applied` in `context.create_issue.safe_outputs` (the issue\nsweny filed after the previous step), else\n`context.create_issue.issueIdentifier` (an existing issue the previous\nstep matched, or one it filed directly).\n\n## Quality bar (non-negotiable)\n\nEvery change you make must clear all of these. If you cannot, STOP and\nreturn a failed status — do not ship a half-measure.\n\n- **Correctness**: the fix actually closes the failure mode in the issue.\n- **Completeness**: handles the obvious edge cases (nulls, empties, off-by-one,\n concurrent paths). If the issue lists multiple symptoms, address them all\n or explicitly state which are out of scope and why.\n- **Idiomatic for THIS repo, not in general.** Read at least one\n neighboring file in the same module before you write any code, and\n match the patterns you see: error-handling primitives the codebase\n already uses, type discipline, naming, file layout. Examples to make\n the shape concrete:\n - In a NestJS service, throwing `NotFoundException` /\n `BadRequestException` is idiomatic; `throw new Error(...)` is not.\n - In a Go service, returning a wrapped `fmt.Errorf(\"...: %w\", err)`\n beats panicking.\n - In a TypeScript repo, narrow types properly — do not `as any` past\n the type checker.\n These are illustrations. Generalize: whatever the repo already does,\n keep doing.\n- **No hacky shortcuts**: no `try/catch` to swallow errors, no commented-out\n guards, no `// TODO: revisit`, no global flags to bypass validation, no\n copy-pasted bodies. If the right fix needs a small refactor (e.g. extract\n a helper), do it; if it needs a large refactor, file a follow-up issue\n and ship the minimal correct fix without the refactor.\n- **Well-tested**: see Test Requirements below.\n\n## Steps\n\n0. **Precondition: staged preview.** If the `issue` entry in\n `context.create_issue.safe_outputs` has status `staged`, this run is a\n preview (`--stage`) and no issue exists. STOP before touching the\n checkout. Output `test_status: \"skipped\"`, `skipped_reason: \"staged\"`,\n empty file lists, `branch: \"\"`, `commit_sha: \"\"`.\n\n0a. **Precondition: verify ISSUE_ID is well-formed.**\n The identifier MUST match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`). If it\n is missing, empty, or matches a Sentry short-ID / slug / any other shape,\n STOP immediately. Do NOT invent a branch name from the Sentry event,\n commit SHA, or alert payload. Return a failed status with a clear message\n instead; a human will pick it up.\n\n0b. **Precondition — check for an existing open PR for this issue.** Call\n `github_search_issues` (which hits GitHub's `/search/issues` endpoint\n and returns both issues and PRs) with `type:pr state:open` and the\n issue identifier wrapped in brackets — e.g. query\n `[OFF-1234] in:title type:pr state:open` and pass `repo` for scoping.\n If the response contains one or more PRs whose title starts with\n `[<issueIdentifier>]`, STOP. Do NOT create a branch, commit, or any\n new PR. Output:\n\n ```\n test_status: \"skipped\"\n skipped_reason: \"open-pr-exists\"\n existing_pr_url: \"<url of the existing PR>\"\n test_files_changed: []\n files_changed: []\n branch: \"\"\n commit_sha: \"\"\n ```\n\n The downstream `create_pr` node will be bypassed and the workflow will\n route to `notify` so the team is told the prior PR is still pending.\n\n Rationale: re-implementing a fix while an open PR already exists\n produces a duplicate (or worse, the agent closes its own prior PR\n mid-run and opens a new one with a different approach — exactly the\n thrash observed in `letsoffload/permit-service` PR #81 → #82 and\n `letsoffload/offload` PR #572 → #573, where two PRs for the same\n identifier were opened-and-closed 12 to 16 seconds apart inside a\n single workflow run).\n\n Only proceed to Step 1 if no open PR exists for the identifier.\n\n1. Create a feature branch from the base branch (check context for\n baseBranch, default `main`). **Branch name MUST include the issue\n identifier** exactly as ISSUE_ID above.\n Format: lowercase the identifier and use it as a prefix, e.g.\n `off-1234-fix-null-check`. If the repository uses a different branch\n convention (check context.branchPattern or the rules/CLAUDE.md for a\n regex), follow that — but the identifier piece MUST still come from\n ISSUE_ID, never from a Sentry short-ID.\n\n2. Read the relevant source files to understand the current code AND read\n the nearest existing test file (e.g. `foo.service.spec.ts` next to\n `foo.service.ts`, or `foo_test.py` next to `foo.py`) so your new tests\n match the existing test conventions: framework, mock style, factory use,\n describe/it shape, naming.\n\n3. Make the necessary code change. Keep it minimal and focused — do not\n refactor surrounding code or add unrelated improvements.\n\n4. **Write tests for the fix.** See Test Requirements below.\n\n5. **Run the test suite for the affected package** and confirm green\n (the package's `test` command from package.json, `pytest` for Python,\n etc.). If the test runner is not obvious, read the project's\n CLAUDE.md / README / package.json scripts. If you cannot run the\n tests at all in this environment, say so explicitly in the output —\n do not claim \"tests pass\" when you didn't run them.\n\n6. Stage and commit with a clear commit message. Start the message with the\n issue identifier in brackets, e.g.\n `[OFF-1234] fix: guard null projectId before database lookup`.\n\n## Scope boundary — do NOT open the PR here\n\nStop after the commit. Do NOT push the branch, do NOT run `gh pr create`,\ndo NOT call any PR-creation tool. The dedicated `create_pr` node pushes the\nbranch and requests the PR. Opening it from this node bypasses the workflow's\neval gate and triggers a self-close-and-recreate workaround (PR opened via\nshell, then `github_create_pr` returns 422 \"already exists\" against the same\nbranch, then the agent closes the first PR to satisfy the eval). The\nidempotent fallback in `github_create_pr` will recover if you ignore this,\nbut the cleaner path is: commit only here, let `create_pr` push and open.\n\n## Test Requirements\n\nA code change without a test is unfinished work. Every fix this node ships\nMUST come with at least one new test case that:\n\n- **Would fail on the unfixed code.** A test that passes both before and\n after the fix is decoration, not a regression guard. Mentally run it\n against the previous version; if it would have passed, it is not the\n right test.\n- **Asserts the user-facing contract**, not just an implementation\n detail. For an API/handler bug: assert the response shape, status code,\n or error type the client sees. For a data corruption bug: assert the\n persisted row's final state, not just an internal call count.\n- **Covers the obvious adjacent edge cases.** If the bug was \"throws plain\n Error on missing record\", add the missing-record case AND at least one\n success-path case to confirm the fix didn't break the happy path.\n- **Lives in the right file** (next to the source under test, in the\n project's existing test layout).\n\nThe eval gate on this node REQUIRES `test_status` to be either\n`pass` (tests ran and all passed) OR `no-framework` (the project\ngenuinely has no test runner — not \"I couldn't be bothered\").\n\nIf the project HAS a test framework, you must write tests and they\nmust pass. Empty `test_files_changed` with `test_status: pass` is a\nlie and eval will reject it via the schema's required-fields\ncontract; `not-run` and `fail` will retry once with reflection and\nthen halt for human attention.\n\nIf the fix turns out to be more complex than expected, stop and\nexplain why — do not force a bad fix.","skills":["github"],"permissions":{"access":"write","deny":["net"]},"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]},"output":{"type":"object","properties":{"branch":{"type":"string","description":"Feature branch name (must include the issue identifier)."},"commit_sha":{"type":"string","description":"SHA of the commit shipping the fix and tests."},"files_changed":{"type":"array","description":"Source files modified by the fix (NOT including test files).","items":{"type":"string"}},"test_files_changed":{"type":"array","description":"Test files added or modified to cover the fix. Should be\nnon-empty whenever `test_status: pass`. May be empty when\n`test_status: no-framework` (genuinely no test runner in\nthe repo).\n","items":{"type":"string"}},"test_command":{"type":"string","description":"Exact command run to validate (e.g. `npx nx test server`)."},"test_status":{"type":"string","enum":["pass","fail","no-framework","not-run","skipped"],"description":"Outcome of running the test suite.\n- `pass` — tests ran end-to-end and every assertion passed.\n `test_files_changed` MUST be non-empty in this case (you\n changed code; tests must cover it).\n- `no-framework` — the repo genuinely has no test runner\n (e.g. a docs-only repo, a YAML-config repo, a research\n notebook collection). Reserved for cases where there is\n no test infrastructure to extend — not a permission slip\n to skip tests in a normal codebase.\n- `fail` — tests ran and at least one failed. Do not push\n a broken fix. Return this status, retry will fire once.\n- `not-run` — tests exist but the environment couldn't\n execute them. Explain in `test_notes`. Retry will fire.\n- `skipped` — the node short-circuited before running tests\n because a precondition was not met: `open-pr-exists` (Step\n 0b found an open PR with the issue identifier in its\n title; `existing_pr_url` MUST be populated) or `staged`\n (Step 0: a preview run filed no issue). `skipped_reason`\n MUST be populated. Downstream `create_pr` is bypassed and\n the workflow routes to `notify`.\n"},"skipped_reason":{"type":"string","description":"Why the node short-circuited. Only set when `test_status` is\n`skipped`: `open-pr-exists` (Step 0b) or `staged` (Step 0).\n","enum":["open-pr-exists","staged"]},"existing_pr_url":{"type":"string","description":"URL of the open PR that triggered the skip. Set when\n`skipped_reason` is `open-pr-exists` so the notify node\ncan link to the prior PR.\n","pattern":"^https?://"},"test_notes":{"type":"string","description":"One-paragraph explanation of which tests were added and what they assert."}},"required":["branch","commit_sha","files_changed","test_files_changed","test_status"]},"eval":[{"name":"test_status_is_pass_or_no_framework","kind":"value","rule":{"output_matches":[{"path":"test_status","in":["pass","no-framework","skipped"]}]}},{"name":"tests_present_when_pass_claimed","kind":"judge","rubric":"You are checking the contract between test_status and test_files_changed.\n\nRead result.data.test_status and result.data.test_files_changed.\n\nRules:\n- If test_status is \"pass\": test_files_changed MUST contain at least\n one real test file path (paths with extensions like .spec.ts,\n .test.ts, .test.tsx, .test.py, _test.go, _spec.rb, .test.rs,\n etc., or paths in a __tests__/, tests/, or spec/ directory).\n An empty array, or paths that are clearly not test files\n (README.md, package.json, source files), is a contract violation.\n- If test_status is \"no-framework\": test_files_changed MAY be empty.\n Pass vacuously.\n- If test_status is \"skipped\": test_files_changed MAY be empty.\n The node short-circuited before writing any code. When\n skipped_reason is \"open-pr-exists\" (Step 0b found an open PR\n for the issue), `existing_pr_url` MUST be populated; when it\n is \"staged\" (a preview run), nothing else is required. If\n skipped_reason is missing, return no.\n- If test_status is \"fail\" or \"not-run\": this evaluator should not\n run (the prior value evaluator catches that). Pass vacuously.\n\nRespond yes if the contract is satisfied, no if it is violated.\n","pass_when":"yes"}],"retry":{"max":1,"instruction":{"auto":true}}},"create_pr":{"name":"Open Pull Request","on_fail":"continue","instruction":"Open a pull request for the fix. The issue is in your context from the create_issue step; its identifier (ISSUE_ID) is the `ref` of the applied `issue` entry in `context.create_issue.safe_outputs`, else `context.create_issue.issueIdentifier`. The branch is `context.implement.branch`.\n\n1. Push the branch to the remote. If the push fails for lack of credentials (a checkout made with `persist-credentials: false`), go on: sweny pushes the head branch itself before it opens the PR.\n2. Request the PR: with `emit_output` (type `pr`, `head` = the branch, `base` = the base branch, default `main`) when you have it; sweny opens it after this step and reuses an open PR for the same branch instead of failing. Without `emit_output`, call `github_create_pr` (use this exact tool name, not `create_pull_request` or any other variant from ToolSearch); if it returns `reused: true`, treat it as success and do NOT close it and call again.\n3. PR title MUST start with the issue identifier in brackets. Format: `[ISSUE_ID] fix: {short description}`.\n4. In the PR body, include: summary of the bug, what the fix does, and a link to the issue (its `url` in `context.create_issue.safe_outputs`, else context.create_issue.issueUrl). If the investigation found a Sentry issue, include \"Fixes {sentryShortId}\" in the PR body so Sentry auto-resolves on merge. If the issue tracker is Linear, also include the line \"Fixes ISSUE_ID\" (e.g. \"Fixes OFF-1234\") so Linear links the PR and moves the issue to Done on merge.\n5. Labels: `[\"sweny\", \"agent\", \"triage\"]`.\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 branch and the PR title you requested (and the PR URL and number when a tool returned them).","skills":["github"],"permissions":"write","outputs":[{"type":"pr","max":1,"labels":["sweny","agent","triage"]}],"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]},"eval":[{"name":"pr_was_created","kind":"function","rule":{"any_tool_called":["emit_output","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.\n Issues and PRs sweny filed are in `safe_outputs` of the create_issue, skip and create_pr steps\n (`ref` and `url` of each `applied` entry).\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n4. If any `safe_outputs` entry has status `staged`, this run is a preview: send nothing, and put the\n notification you would have sent in your summary.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"]}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":{"expr":"investigate.novel_count > 0 && investigate.highest_severity in ['medium', 'high', 'critical']"},"description":"at least one novel finding, and the highest severity is medium or higher"},{"from":"investigate","to":"skip","when":{"expr":"investigate.novel_count == 0 || investigate.highest_severity == 'low'"},"description":"no novel findings, or the highest severity is low"},{"from":"create_issue","to":"implement","when":{"expr":"investigate.fixable_count > 0"},"description":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","description":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"notify","when":{"expr":"implement.test_status == 'skipped'"},"description":"the implement step was skipped (an open PR already exists, or a staged preview was found)"},{"from":"implement","to":"create_pr","when":{"expr":"implement.test_status in ['pass', 'no-framework']"},"description":"the fix is committed and its tests passed (or the repo has no test framework)"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
2
|
-
export const implementWorkflow = {"id":"implement","name":"Implement Fix","workflow_type":"pr_review","description":"Implement a code fix for an issue and open a pull request","entry":"analyze","permissions":"write","safe_outputs":{"allow":["comment","pr"],"max":3},"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. **Check for an existing open PR for this issue.** Call\n `github_search_issues` (GitHub's `/search/issues` endpoint returns\n both issues and PRs) with `type:pr state:open` and the issue\n identifier wrapped in brackets — e.g. query\n `[OFF-1234] in:title type:pr state:open`. If the response contains\n one or more PRs whose title starts with `[<issueIdentifier>]`,\n STOP analysis. Output `has_open_pr: true`, `existing_pr_url`\n (the URL of the open PR) and a short `issue_summary` noting the\n skip; leave `fix_plan` set to a one-line \"Skip: open PR\n <url> already exists\". The workflow will route to `notify` and\n the team will be told the prior PR is still pending.\n Rationale: re-implementing while an open PR exists produces a\n duplicate (or worse, the agent closes its own prior PR mid-run\n and opens a new one — exactly the thrash seen in\n `letsoffload/permit-service` PR #81 → #82 and\n `letsoffload/offload` PR #572 → #573).\n3. Read the relevant source files to understand the current code.\n4. Identify the exact files and lines that need to change.\n5. Plan the fix approach.\n\nOutput a clear analysis of what needs to change and why, plus three\nrouting fields: `has_open_pr` (true only when step 2 found an open PR),\n`risk_level` (low, medium or high), and `plan_is_clear` (true only when\nthe plan names the exact files and changes). sweny implements the fix\nonly when there is no open PR, the risk is low or medium, and the plan\nis clear; otherwise it skips.","skills":["github","linear"],"permissions":"read","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"]},"existing_pr_url":{"type":"string","description":"URL of an open PR found in step 2 whose title starts with the\nissue identifier in brackets. Set ONLY when a prior PR exists\nand the analyze node is short-circuiting. Triggers the\n`analyze → notify` edge.\n","pattern":"^https?://"},"has_open_pr":{"type":"boolean","description":"true when step 2 found an open PR for this issue (and\nexisting_pr_url is set), else false. Routes analyze to notify.\n"},"plan_is_clear":{"type":"boolean","description":"true when fix_plan names the exact files and changes, so the fix\ncan be implemented without guessing; false when the fix is\nunclear or needs a decision first.\n"}},"required":["issue_summary","fix_plan","has_open_pr","risk_level","plan_is_clear"]}},"implement":{"name":"Implement Fix","instruction":"Implement the planned fix:\n\n1. Create a feature branch. The branch name MUST start with the issue\n identifier from `context.input.issueIdentifier`, lowercased, followed\n by a short kebab-case description: e.g. `off-1234-fix-null-check`.\n2. Make the necessary code changes.\n3. Ensure changes are minimal and focused — fix the bug, nothing more.\n4. Stage and commit. Start the commit message with the issue identifier\n in brackets, e.g. `[OFF-1234] fix: guard null projectId`.\n\nStop after the commit. Do NOT push or open the PR: the dedicated `create_pr`\nnode pushes the branch and requests the PR.\n\nIf the fix is too risky or complex, explain why and skip.","skills":["github"],"permissions":{"access":"write","deny":["net"]},"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]}},"create_pr":{"name":"Open Pull Request","on_fail":"continue","instruction":"Open a pull request for the fix:\n\n1. Push the branch to the remote. If the push fails for lack of credentials (a checkout made with `persist-credentials: false`), go on: sweny pushes the head branch itself before it opens the PR.\n2. Request the PR: with `emit_output` (type `pr`, `head` = the branch, `base` = the base branch, default `main`) when you have it; sweny opens it after this step and reuses an open PR for the same branch instead of failing. Without `emit_output`, call `github_create_pr`. The PR title MUST start with the issue identifier in brackets: `[{context.input.issueIdentifier}] type: short description` (e.g. `[OFF-1234] fix: guard null projectId`).\n3. Reference the original issue in the PR body. If the issue tracker is Linear, include the line \"Fixes {context.input.issueIdentifier}\" (e.g. \"Fixes OFF-1234\") so Linear links the PR and moves the issue to Done on merge.\n4. Labels: add `task` (or `feature` if the issue is a feature request); `sweny` and `agent` are always added.\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 branch and the PR title you requested.","skills":["github"],"permissions":"write","outputs":[{"type":"pr","max":1,"labels":["sweny","agent"]}],"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]}},"notify":{"name":"Notify","instruction":"Send a notification about the implementation result:\n\n1. Include: issue reference, PR link, brief summary of changes. The PR sweny opened is in\n `context.create_pr.safe_outputs` (`ref` and `url` of the `applied` entry).\n2. Keep it concise: one message, not a wall of text.\n3. If any `safe_outputs` entry has status `staged`, this run is a preview: send nothing, and put the\n notification you would have sent in your summary.","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\n(with `emit_output` type `comment` when you have it).","skills":["linear","github"],"outputs":[{"type":"comment","max":1,"number":{"input":"issueIdentifier"}}]}},"edges":[{"from":"analyze","to":"notify","when":{"expr":"analyze.has_open_pr == true || exists analyze.existing_pr_url"},"description":"an open PR for this issue already exists (analyze.existing_pr_url is set); skip the implement and create_pr nodes"},{"from":"analyze","to":"implement","when":{"expr":"analyze.has_open_pr == false && !(exists analyze.existing_pr_url) && analyze.risk_level in ['low', 'medium'] && analyze.plan_is_clear == true"},"description":"no open PR exists, the fix risk level is low or medium, and a clear plan exists"},{"from":"analyze","to":"skip","description":"the fix is too complex, risky, or unclear"},{"from":"implement","to":"create_pr"},{"from":"skip","to":"notify"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
3
|
-
export const seedContentWorkflow = {"id":"seed-content","name":"Content Generation Pipeline","workflow_type":"content_generation","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":{}};
|
|
1
|
+
export const triageWorkflow = {"spec_version":"2","id":"triage","name":"Alert Triage","workflow_type":"monitor","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","permissions":"write","safe_outputs":{"allow":["issue","comment","issue_state","pr"],"max":25},"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, but stay in scope: read, search, and summarize. The investigation step depends on complete context, not on fixes.\n\n**Scope boundary (this node is READ-ONLY):**\n- DO NOT create issues, comments, branches, commits, or pull requests, and DO NOT implement fixes. Downstream nodes (`create_issue`, `implement`, `create_pr`) handle ALL writes.\n- Your ONLY job is to collect context and report it, then stop. Doing downstream work here burns your turn budget and duplicates what later nodes will do.","skills":["github","sentry","datadog","betterstack","linear"],"permissions":"read","fail_soft":true},"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 eval 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- `fixable_count`: how many NOVEL findings have `fix_complexity` simple or moderate AND a non-empty `fix_approach`. 0 when none do. This decides whether sweny attempts a fix after filing the issue.\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 eval check will fail or duplicate work.","skills":["github","linear"],"permissions":"read","eval":[{"name":"novelty_search_was_performed","kind":"function","rule":{"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"},"fixable_count":{"type":"integer","minimum":0,"description":"Count of novel findings whose fix_complexity is simple or moderate AND whose fix_approach is a non-empty plan. Routes create_issue to implement.","x-sweny-count-of":{"array":"findings","where":{"is_duplicate":[false],"fix_complexity":["simple","moderate"]},"non_empty":["fix_approach"]}},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","fixable_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**How writes work here.** If you have the `emit_output` tool, you cannot write to the tracker directly: request each new issue and each comment with `emit_output`, and sweny files them after this step, within its limits. A requested issue has no identifier yet; sweny records it for the next steps. Without `emit_output`, use the tracker's own tools (for example `linear_create_issue`, `github_create_issue`, `linear_add_comment`, `github_add_comment`, or the configured tracker's MCP tools) and copy the identifier they return.\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.\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, file a new one with a clear, actionable title (type `issue`). Do NOT synthesize an identifier from an alert ID, commit SHA, or your own imagination: a new issue's identifier comes only from the tracker. Set `dedupe_key` to a short stable fingerprint of the root cause, so two findings with the same cause are filed once. Set the `action` to `requested` (or `created` when you filed it with a tracker tool and have its identifier).\n3. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n4. Apply the `sweny`, `agent`, and `triage` labels. For Linear: call `linear_list_labels` to find the IDs for labels named `sweny`, `agent`, and `triage`, and pass those IDs as the labels (skip any that don't exist in the team; don't invent IDs). For Linear, the target is the team ID (`linear_list_teams`). For GitHub: labels `[\"sweny\", \"agent\", \"triage\"]`.\n5. Link to relevant commits, PRs, or existing issues.\n6. When you know the identifier (an existing issue, or one a tracker tool returned), copy it verbatim into `issueIdentifier`. It must match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`); do not prefix it with anything else. Leave `issueIdentifier` unset for an issue you only requested.\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 (type `comment`, `number` = the issue ID or number): \"+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 or done: reopen it (type `issue_state`, `state` = `reopen`, `number` = the issue ID or number) when you have `emit_output`; with a tracker tool instead, use one that can reopen. If you can do neither, say in the comment that it is closed and was seen again.\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 issues you requested, created, or updated.\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- Your job is issue creation and duplicate management ONLY.","skills":["linear","github"],"outputs":[{"type":"issue","max":10},{"type":"comment","max":10},{"type":"issue_state","state":"reopen","max":10}],"tools":{"deny":["github_create_pr"]},"eval":[{"name":"issue_tracker_tool_was_called","kind":"function","rule":{"any_tool_called":["emit_output","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) when known in this step: an existing issue the duplicate check matched, or one a tracker tool returned. Unset for an issue only requested through emit_output; sweny records its identifier in safe_outputs. Never invented from a Sentry short-ID or commit SHA."},"issueTitle":{"type":"string","description":"Title of the primary issue"},"issueUrl":{"type":"string","pattern":"^https?://","description":"URL of the primary issue, when known in this step"},"issues":{"type":"array","minItems":1,"description":"All issues requested, created, or updated during this step","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"action":{"type":"string","enum":["requested","created","updated","reopened"]}},"required":["action"]}}},"required":["issueTitle","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 (with `emit_output` type `comment` when you have it, `number` = the issue ID or number): \"+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 or done: reopen it (`emit_output` type `issue_state`, `state` = `reopen`, `number` = the issue ID or number) when you have `emit_output`; with a tracker tool instead, use one that can reopen. If you can do neither, say in the comment that it is closed and was seen again.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"],"outputs":[{"type":"comment","max":10},{"type":"issue_state","state":"reopen","max":10}]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation, plus the tests that pin\nit as a regression guard. The issue from the previous step is in your\ncontext. Use its identifier for branch naming and commit messages.\n\n**The issue identifier** (ISSUE_ID below): the `ref` of the `issue` entry\nwith status `applied` in `context.create_issue.safe_outputs` (the issue\nsweny filed after the previous step), else\n`context.create_issue.issueIdentifier` (an existing issue the previous\nstep matched, or one it filed directly).\n\n## Quality bar (non-negotiable)\n\nEvery change you make must clear all of these. If you cannot, STOP and\nreturn a failed status — do not ship a half-measure.\n\n- **Correctness**: the fix actually closes the failure mode in the issue.\n- **Completeness**: handles the obvious edge cases (nulls, empties, off-by-one,\n concurrent paths). If the issue lists multiple symptoms, address them all\n or explicitly state which are out of scope and why.\n- **Idiomatic for THIS repo, not in general.** Read at least one\n neighboring file in the same module before you write any code, and\n match the patterns you see: error-handling primitives the codebase\n already uses, type discipline, naming, file layout. Examples to make\n the shape concrete:\n - In a NestJS service, throwing `NotFoundException` /\n `BadRequestException` is idiomatic; `throw new Error(...)` is not.\n - In a Go service, returning a wrapped `fmt.Errorf(\"...: %w\", err)`\n beats panicking.\n - In a TypeScript repo, narrow types properly — do not `as any` past\n the type checker.\n These are illustrations. Generalize: whatever the repo already does,\n keep doing.\n- **No hacky shortcuts**: no `try/catch` to swallow errors, no commented-out\n guards, no `// TODO: revisit`, no global flags to bypass validation, no\n copy-pasted bodies. If the right fix needs a small refactor (e.g. extract\n a helper), do it; if it needs a large refactor, file a follow-up issue\n and ship the minimal correct fix without the refactor.\n- **Well-tested**: see Test Requirements below.\n\n## Steps\n\n0. **Precondition: staged preview.** If the `issue` entry in\n `context.create_issue.safe_outputs` has status `staged`, this run is a\n preview (`--stage`) and no issue exists. STOP before touching the\n checkout. Output `test_status: \"skipped\"`, `skipped_reason: \"staged\"`,\n empty file lists, `branch: \"\"`, `commit_sha: \"\"`.\n\n0a. **Precondition: verify ISSUE_ID is well-formed.**\n The identifier MUST match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`). If it\n is missing, empty, or matches a Sentry short-ID / slug / any other shape,\n STOP immediately. Do NOT invent a branch name from the Sentry event,\n commit SHA, or alert payload. Return a failed status with a clear message\n instead; a human will pick it up.\n\n0b. **Precondition — check for an existing open PR for this issue.** Call\n `github_search_issues` (which hits GitHub's `/search/issues` endpoint\n and returns both issues and PRs) with `type:pr state:open` and the\n issue identifier wrapped in brackets — e.g. query\n `[OFF-1234] in:title type:pr state:open` and pass `repo` for scoping.\n If the response contains one or more PRs whose title starts with\n `[<issueIdentifier>]`, STOP. Do NOT create a branch, commit, or any\n new PR. Output:\n\n ```\n test_status: \"skipped\"\n skipped_reason: \"open-pr-exists\"\n existing_pr_url: \"<url of the existing PR>\"\n test_files_changed: []\n files_changed: []\n branch: \"\"\n commit_sha: \"\"\n ```\n\n The downstream `create_pr` node will be bypassed and the workflow will\n route to `notify` so the team is told the prior PR is still pending.\n\n Rationale: re-implementing a fix while an open PR already exists\n produces a duplicate (or worse, the agent closes its own prior PR\n mid-run and opens a new one with a different approach — exactly the\n thrash observed in `letsoffload/permit-service` PR #81 → #82 and\n `letsoffload/offload` PR #572 → #573, where two PRs for the same\n identifier were opened-and-closed 12 to 16 seconds apart inside a\n single workflow run).\n\n Only proceed to Step 1 if no open PR exists for the identifier.\n\n1. Create a feature branch from the base branch (check context for\n baseBranch, default `main`). **Branch name MUST include the issue\n identifier** exactly as ISSUE_ID above.\n Format: lowercase the identifier and use it as a prefix, e.g.\n `off-1234-fix-null-check`. If the repository uses a different branch\n convention (check context.branchPattern or the rules/CLAUDE.md for a\n regex), follow that — but the identifier piece MUST still come from\n ISSUE_ID, never from a Sentry short-ID.\n\n2. Read the relevant source files to understand the current code AND read\n the nearest existing test file (e.g. `foo.service.spec.ts` next to\n `foo.service.ts`, or `foo_test.py` next to `foo.py`) so your new tests\n match the existing test conventions: framework, mock style, factory use,\n describe/it shape, naming.\n\n3. Make the necessary code change. Keep it minimal and focused — do not\n refactor surrounding code or add unrelated improvements.\n\n4. **Write tests for the fix.** See Test Requirements below.\n\n5. **Run the test suite for the affected package** and confirm green\n (the package's `test` command from package.json, `pytest` for Python,\n etc.). If the test runner is not obvious, read the project's\n CLAUDE.md / README / package.json scripts. If you cannot run the\n tests at all in this environment, say so explicitly in the output —\n do not claim \"tests pass\" when you didn't run them.\n\n6. Stage and commit with a clear commit message. Start the message with the\n issue identifier in brackets, e.g.\n `[OFF-1234] fix: guard null projectId before database lookup`.\n\n## Scope boundary — do NOT open the PR here\n\nStop after the commit. Do NOT push the branch, do NOT run `gh pr create`,\ndo NOT call any PR-creation tool. The dedicated `create_pr` node pushes the\nbranch and requests the PR. Opening it from this node bypasses the workflow's\neval gate and triggers a self-close-and-recreate workaround (PR opened via\nshell, then `github_create_pr` returns 422 \"already exists\" against the same\nbranch, then the agent closes the first PR to satisfy the eval). The\nidempotent fallback in `github_create_pr` will recover if you ignore this,\nbut the cleaner path is: commit only here, let `create_pr` push and open.\n\n## Test Requirements\n\nA code change without a test is unfinished work. Every fix this node ships\nMUST come with at least one new test case that:\n\n- **Would fail on the unfixed code.** A test that passes both before and\n after the fix is decoration, not a regression guard. Mentally run it\n against the previous version; if it would have passed, it is not the\n right test.\n- **Asserts the user-facing contract**, not just an implementation\n detail. For an API/handler bug: assert the response shape, status code,\n or error type the client sees. For a data corruption bug: assert the\n persisted row's final state, not just an internal call count.\n- **Covers the obvious adjacent edge cases.** If the bug was \"throws plain\n Error on missing record\", add the missing-record case AND at least one\n success-path case to confirm the fix didn't break the happy path.\n- **Lives in the right file** (next to the source under test, in the\n project's existing test layout).\n\nThe eval gate on this node REQUIRES `test_status` to be either\n`pass` (tests ran and all passed) OR `no-framework` (the project\ngenuinely has no test runner — not \"I couldn't be bothered\").\n\nIf the project HAS a test framework, you must write tests and they\nmust pass. Empty `test_files_changed` with `test_status: pass` is a\nlie and eval will reject it via the schema's required-fields\ncontract; `not-run` and `fail` will retry once with reflection and\nthen halt for human attention.\n\nIf the fix turns out to be more complex than expected, stop and\nexplain why — do not force a bad fix.","skills":["github"],"permissions":{"access":"write","deny":["net"]},"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]},"output":{"type":"object","properties":{"branch":{"type":"string","description":"Feature branch name (must include the issue identifier)."},"commit_sha":{"type":"string","description":"SHA of the commit shipping the fix and tests."},"files_changed":{"type":"array","description":"Source files modified by the fix (NOT including test files).","items":{"type":"string"}},"test_files_changed":{"type":"array","description":"Test files added or modified to cover the fix. Should be\nnon-empty whenever `test_status: pass`. May be empty when\n`test_status: no-framework` (genuinely no test runner in\nthe repo).\n","items":{"type":"string"}},"test_command":{"type":"string","description":"Exact command run to validate (e.g. `npx nx test server`)."},"test_status":{"type":"string","enum":["pass","fail","no-framework","not-run","skipped"],"description":"Outcome of running the test suite.\n- `pass` — tests ran end-to-end and every assertion passed.\n `test_files_changed` MUST be non-empty in this case (you\n changed code; tests must cover it).\n- `no-framework` — the repo genuinely has no test runner\n (e.g. a docs-only repo, a YAML-config repo, a research\n notebook collection). Reserved for cases where there is\n no test infrastructure to extend — not a permission slip\n to skip tests in a normal codebase.\n- `fail` — tests ran and at least one failed. Do not push\n a broken fix. Return this status, retry will fire once.\n- `not-run` — tests exist but the environment couldn't\n execute them. Explain in `test_notes`. Retry will fire.\n- `skipped` — the node short-circuited before running tests\n because a precondition was not met: `open-pr-exists` (Step\n 0b found an open PR with the issue identifier in its\n title; `existing_pr_url` MUST be populated) or `staged`\n (Step 0: a preview run filed no issue). `skipped_reason`\n MUST be populated. Downstream `create_pr` is bypassed and\n the workflow routes to `notify`.\n"},"skipped_reason":{"type":"string","description":"Why the node short-circuited. Only set when `test_status` is\n`skipped`: `open-pr-exists` (Step 0b) or `staged` (Step 0).\n","enum":["open-pr-exists","staged"]},"existing_pr_url":{"type":"string","description":"URL of the open PR that triggered the skip. Set when\n`skipped_reason` is `open-pr-exists` so the notify node\ncan link to the prior PR.\n","pattern":"^https?://"},"test_notes":{"type":"string","description":"One-paragraph explanation of which tests were added and what they assert."}},"required":["branch","commit_sha","files_changed","test_files_changed","test_status"]},"eval":[{"name":"test_status_is_pass_or_no_framework","kind":"value","rule":{"output_matches":[{"path":"test_status","in":["pass","no-framework","skipped"]}]}},{"name":"tests_present_when_pass_claimed","kind":"judge","rubric":"You are checking the contract between test_status and test_files_changed.\n\nRead result.data.test_status and result.data.test_files_changed.\n\nRules:\n- If test_status is \"pass\": test_files_changed MUST contain at least\n one real test file path (paths with extensions like .spec.ts,\n .test.ts, .test.tsx, .test.py, _test.go, _spec.rb, .test.rs,\n etc., or paths in a __tests__/, tests/, or spec/ directory).\n An empty array, or paths that are clearly not test files\n (README.md, package.json, source files), is a contract violation.\n- If test_status is \"no-framework\": test_files_changed MAY be empty.\n Pass vacuously.\n- If test_status is \"skipped\": test_files_changed MAY be empty.\n The node short-circuited before writing any code. When\n skipped_reason is \"open-pr-exists\" (Step 0b found an open PR\n for the issue), `existing_pr_url` MUST be populated; when it\n is \"staged\" (a preview run), nothing else is required. If\n skipped_reason is missing, return no.\n- If test_status is \"fail\" or \"not-run\": this evaluator should not\n run (the prior value evaluator catches that). Pass vacuously.\n\nRespond yes if the contract is satisfied, no if it is violated.\n","pass_when":"yes"}],"retry":{"max":1,"instruction":{"auto":true}}},"create_pr":{"name":"Open Pull Request","on_fail":"continue","instruction":"Open a pull request for the fix. The issue is in your context from the create_issue step; its identifier (ISSUE_ID) is the `ref` of the applied `issue` entry in `context.create_issue.safe_outputs`, else `context.create_issue.issueIdentifier`. The branch is `context.implement.branch`.\n\n1. Push the branch to the remote. If the push fails for lack of credentials (a checkout made with `persist-credentials: false`), go on: sweny pushes the head branch itself before it opens the PR.\n2. Request the PR: with `emit_output` (type `pr`, `head` = the branch, `base` = the base branch, default `main`) when you have it; sweny opens it after this step and reuses an open PR for the same branch instead of failing. Without `emit_output`, call `github_create_pr` (use this exact tool name, not `create_pull_request` or any other variant from ToolSearch); if it returns `reused: true`, treat it as success and do NOT close it and call again.\n3. PR title MUST start with the issue identifier in brackets. Format: `[ISSUE_ID] fix: {short description}`.\n4. In the PR body, include: summary of the bug, what the fix does, and a link to the issue (its `url` in `context.create_issue.safe_outputs`, else context.create_issue.issueUrl). If the investigation found a Sentry issue, include \"Fixes {sentryShortId}\" in the PR body so Sentry auto-resolves on merge. If the issue tracker is Linear, also include the line \"Fixes ISSUE_ID\" (e.g. \"Fixes OFF-1234\") so Linear links the PR and moves the issue to Done on merge.\n5. Labels: `[\"sweny\", \"agent\", \"triage\"]`.\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 branch and the PR title you requested (and the PR URL and number when a tool returned them).","skills":["github"],"permissions":"write","outputs":[{"type":"pr","max":1,"labels":["sweny","agent","triage"]}],"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]},"eval":[{"name":"pr_was_created","kind":"function","rule":{"any_tool_called":["emit_output","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.\n Issues and PRs sweny filed are in `safe_outputs` of the create_issue, skip and create_pr steps\n (`ref` and `url` of each `applied` entry).\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n4. If any `safe_outputs` entry has status `staged`, this run is a preview: send nothing, and put the\n notification you would have sent in your summary.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"],"permissions":{"access":"write","deny":["shell","net","subagent"]}}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":{"expr":"investigate.novel_count > 0 && investigate.highest_severity in ['medium', 'high', 'critical']"},"description":"at least one novel finding, and the highest severity is medium or higher"},{"from":"investigate","to":"skip","when":{"expr":"investigate.novel_count == 0 || investigate.highest_severity == 'low'"},"description":"no novel findings, or the highest severity is low"},{"from":"create_issue","to":"implement","when":{"expr":"investigate.fixable_count > 0"},"description":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","description":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"notify","when":{"expr":"implement.test_status == 'skipped'"},"description":"the implement step was skipped (an open PR already exists, or a staged preview was found)"},{"from":"implement","to":"create_pr","when":{"expr":"implement.test_status in ['pass', 'no-framework']"},"description":"the fix is committed and its tests passed (or the repo has no test framework)"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
2
|
+
export const implementWorkflow = {"spec_version":"2","id":"implement","name":"Implement Fix","workflow_type":"pr_review","description":"Implement a code fix for an issue and open a pull request","entry":"analyze","permissions":"write","safe_outputs":{"allow":["comment","pr"],"max":3},"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. **Check for an existing open PR for this issue.** Call\n `github_search_issues` (GitHub's `/search/issues` endpoint returns\n both issues and PRs) with `type:pr state:open` and the issue\n identifier wrapped in brackets — e.g. query\n `[OFF-1234] in:title type:pr state:open`. If the response contains\n one or more PRs whose title starts with `[<issueIdentifier>]`,\n STOP analysis. Output `has_open_pr: true`, `existing_pr_url`\n (the URL of the open PR) and a short `issue_summary` noting the\n skip; leave `fix_plan` set to a one-line \"Skip: open PR\n <url> already exists\". The workflow will route to `notify` and\n the team will be told the prior PR is still pending.\n Rationale: re-implementing while an open PR exists produces a\n duplicate (or worse, the agent closes its own prior PR mid-run\n and opens a new one — exactly the thrash seen in\n `letsoffload/permit-service` PR #81 → #82 and\n `letsoffload/offload` PR #572 → #573).\n3. Read the relevant source files to understand the current code.\n4. Identify the exact files and lines that need to change.\n5. Plan the fix approach.\n\nOutput a clear analysis of what needs to change and why, plus three\nrouting fields: `has_open_pr` (true only when step 2 found an open PR),\n`risk_level` (low, medium or high), and `plan_is_clear` (true only when\nthe plan names the exact files and changes). sweny implements the fix\nonly when there is no open PR, the risk is low or medium, and the plan\nis clear; otherwise it skips.","skills":["github","linear"],"permissions":"read","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"]},"existing_pr_url":{"type":"string","description":"URL of an open PR found in step 2 whose title starts with the\nissue identifier in brackets. Set ONLY when a prior PR exists\nand the analyze node is short-circuiting. Triggers the\n`analyze → notify` edge.\n","pattern":"^https?://"},"has_open_pr":{"type":"boolean","description":"true when step 2 found an open PR for this issue (and\nexisting_pr_url is set), else false. Routes analyze to notify.\n"},"plan_is_clear":{"type":"boolean","description":"true when fix_plan names the exact files and changes, so the fix\ncan be implemented without guessing; false when the fix is\nunclear or needs a decision first.\n"}},"required":["issue_summary","fix_plan","has_open_pr","risk_level","plan_is_clear"]}},"implement":{"name":"Implement Fix","instruction":"Implement the planned fix:\n\n1. Create a feature branch. The branch name MUST start with the issue\n identifier from `context.input.issueIdentifier`, lowercased, followed\n by a short kebab-case description: e.g. `off-1234-fix-null-check`.\n2. Make the necessary code changes.\n3. Ensure changes are minimal and focused — fix the bug, nothing more.\n4. Stage and commit. Start the commit message with the issue identifier\n in brackets, e.g. `[OFF-1234] fix: guard null projectId`.\n\nStop after the commit. Do NOT push or open the PR: the dedicated `create_pr`\nnode pushes the branch and requests the PR.\n\nIf the fix is too risky or complex, explain why and skip.","skills":["github"],"permissions":{"access":"write","deny":["net"]},"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]}},"create_pr":{"name":"Open Pull Request","on_fail":"continue","instruction":"Open a pull request for the fix:\n\n1. Push the branch to the remote. If the push fails for lack of credentials (a checkout made with `persist-credentials: false`), go on: sweny pushes the head branch itself before it opens the PR.\n2. Request the PR: with `emit_output` (type `pr`, `head` = the branch, `base` = the base branch, default `main`) when you have it; sweny opens it after this step and reuses an open PR for the same branch instead of failing. Without `emit_output`, call `github_create_pr`. The PR title MUST start with the issue identifier in brackets: `[{context.input.issueIdentifier}] type: short description` (e.g. `[OFF-1234] fix: guard null projectId`).\n3. Reference the original issue in the PR body. If the issue tracker is Linear, include the line \"Fixes {context.input.issueIdentifier}\" (e.g. \"Fixes OFF-1234\") so Linear links the PR and moves the issue to Done on merge.\n4. Labels: add `task` (or `feature` if the issue is a feature request); `sweny` and `agent` are always added.\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 branch and the PR title you requested.","skills":["github"],"permissions":"write","outputs":[{"type":"pr","max":1,"labels":["sweny","agent"]}],"tools":{"deny":["github_create_pr","github_create_issue","github_add_comment","github_add_labels"]}},"notify":{"name":"Notify","instruction":"Send a notification about the implementation result:\n\n1. Include: issue reference, PR link, brief summary of changes. The PR sweny opened is in\n `context.create_pr.safe_outputs` (`ref` and `url` of the `applied` entry).\n2. Keep it concise: one message, not a wall of text.\n3. If any `safe_outputs` entry has status `staged`, this run is a preview: send nothing, and put the\n notification you would have sent in your summary.","skills":["slack","notification"],"permissions":{"access":"write","deny":["shell","net","subagent"]}},"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\n(with `emit_output` type `comment` when you have it).","skills":["linear","github"],"outputs":[{"type":"comment","max":1,"number":{"input":"issueIdentifier"}}]}},"edges":[{"from":"analyze","to":"notify","when":{"expr":"analyze.has_open_pr == true || exists analyze.existing_pr_url"},"description":"an open PR for this issue already exists (analyze.existing_pr_url is set); skip the implement and create_pr nodes"},{"from":"analyze","to":"implement","when":{"expr":"analyze.has_open_pr == false && !(exists analyze.existing_pr_url) && analyze.risk_level in ['low', 'medium'] && analyze.plan_is_clear == true"},"description":"no open PR exists, the fix risk level is low or medium, and a clear plan exists"},{"from":"analyze","to":"skip","description":"the fix is too complex, risky, or unclear"},{"from":"implement","to":"create_pr"},{"from":"skip","to":"notify"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
3
|
+
export const seedContentWorkflow = {"spec_version":"2","id":"seed-content","name":"Content Generation Pipeline","workflow_type":"content_generation","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"],"permissions":{"access":"write","deny":["shell","write","edit","net","subagent"]},"tools":{"allow":["supabase_insert"]},"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"],"permissions":{"access":"write","deny":["shell","write","edit","net","subagent"]},"tools":{"allow":["supabase_query","supabase_update"]},"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":{}};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
spec_version: "2"
|
|
1
2
|
id: implement
|
|
2
3
|
name: Implement Fix
|
|
3
4
|
workflow_type: pr_review
|
|
@@ -186,6 +187,12 @@ nodes:
|
|
|
186
187
|
skills:
|
|
187
188
|
- slack
|
|
188
189
|
- notification
|
|
190
|
+
# #598: write only for the notification. The slack and notification skills
|
|
191
|
+
# hold nothing but notification tools; no shell, network or subagents.
|
|
192
|
+
# File writes stay for a notification written to a file.
|
|
193
|
+
permissions:
|
|
194
|
+
access: write
|
|
195
|
+
deny: [shell, net, subagent]
|
|
189
196
|
skip:
|
|
190
197
|
name: Skip - Too Complex
|
|
191
198
|
instruction: |-
|