@sweny-ai/core 0.1.70 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/executor.test.js +90 -0
- package/dist/__tests__/schema.test.js +14 -0
- package/dist/__tests__/skills.test.js +18 -1
- package/dist/executor.js +34 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.js +8 -0
- package/dist/skills/linear.js +12 -2
- package/dist/types.d.ts +22 -0
- package/dist/workflows/browser.js +1 -1
- package/dist/workflows/triage.yml +38 -6
- package/package.json +1 -1
|
@@ -448,4 +448,94 @@ describe("executor", () => {
|
|
|
448
448
|
// Should still complete — falls back to first valid edge target
|
|
449
449
|
expect(results.size).toBe(2);
|
|
450
450
|
});
|
|
451
|
+
describe("node.verify", () => {
|
|
452
|
+
// Context: a create_issue node can satisfy its output schema (issueIdentifier
|
|
453
|
+
// present) while never actually calling the Linear/GitHub create-issue tool.
|
|
454
|
+
// The verify block catches this by requiring a real, successful tool call.
|
|
455
|
+
const verifyWorkflow = {
|
|
456
|
+
id: "verify-create-issue",
|
|
457
|
+
name: "Verify Create Issue",
|
|
458
|
+
description: "Single node that must call linear_create_issue",
|
|
459
|
+
entry: "create",
|
|
460
|
+
nodes: {
|
|
461
|
+
create: {
|
|
462
|
+
name: "Create",
|
|
463
|
+
instruction: "Create an issue",
|
|
464
|
+
skills: [],
|
|
465
|
+
verify: { any_tool_called: ["linear_create_issue", "github_create_issue"] },
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
edges: [],
|
|
469
|
+
};
|
|
470
|
+
it("marks node failed when required tool was never called", async () => {
|
|
471
|
+
const claude = {
|
|
472
|
+
async run() {
|
|
473
|
+
return {
|
|
474
|
+
status: "success",
|
|
475
|
+
data: { issueIdentifier: "OFF-9999", issueTitle: "hallucinated", issueUrl: "https://" },
|
|
476
|
+
toolCalls: [{ tool: "linear_search_issues", input: {}, output: [] }],
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
async evaluate(opts) {
|
|
480
|
+
return opts.choices[0]?.id;
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
|
|
484
|
+
const r = results.get("create");
|
|
485
|
+
expect(r.status).toBe("failed");
|
|
486
|
+
expect(r.data.error).toMatch(/verify\.any_tool_called failed/);
|
|
487
|
+
expect(r.data.error).toMatch(/linear_create_issue/);
|
|
488
|
+
});
|
|
489
|
+
it("marks node failed when required tool erred (output.error set)", async () => {
|
|
490
|
+
const claude = {
|
|
491
|
+
async run() {
|
|
492
|
+
return {
|
|
493
|
+
status: "success",
|
|
494
|
+
data: { issueIdentifier: "OFF-9999" },
|
|
495
|
+
toolCalls: [{ tool: "linear_create_issue", input: {}, output: { error: "Linear API 403" } }],
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
async evaluate(opts) {
|
|
499
|
+
return opts.choices[0]?.id;
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
|
|
503
|
+
expect(results.get("create").status).toBe("failed");
|
|
504
|
+
});
|
|
505
|
+
it("passes when a required tool was invoked successfully", async () => {
|
|
506
|
+
const claude = {
|
|
507
|
+
async run() {
|
|
508
|
+
return {
|
|
509
|
+
status: "success",
|
|
510
|
+
data: { issueIdentifier: "OFF-1234" },
|
|
511
|
+
toolCalls: [
|
|
512
|
+
{
|
|
513
|
+
tool: "linear_create_issue",
|
|
514
|
+
input: { title: "t" },
|
|
515
|
+
output: { issueCreate: { success: true, issue: { identifier: "OFF-1234" } } },
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
};
|
|
519
|
+
},
|
|
520
|
+
async evaluate(opts) {
|
|
521
|
+
return opts.choices[0]?.id;
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
|
|
525
|
+
expect(results.get("create").status).toBe("success");
|
|
526
|
+
});
|
|
527
|
+
it("leaves already-failed nodes alone (does not overwrite Claude errors)", async () => {
|
|
528
|
+
const claude = {
|
|
529
|
+
async run() {
|
|
530
|
+
return { status: "failed", data: { error: "max_turns" }, toolCalls: [] };
|
|
531
|
+
},
|
|
532
|
+
async evaluate(opts) {
|
|
533
|
+
return opts.choices[0]?.id;
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
const { results } = await execute(verifyWorkflow, {}, { skills: createSkillMap([]), claude, config: {} });
|
|
537
|
+
expect(results.get("create").status).toBe("failed");
|
|
538
|
+
expect(results.get("create").data.error).toBe("max_turns");
|
|
539
|
+
});
|
|
540
|
+
});
|
|
451
541
|
});
|
|
@@ -43,6 +43,20 @@ describe("Zod schemas", () => {
|
|
|
43
43
|
});
|
|
44
44
|
expect(result.output).toBeDefined();
|
|
45
45
|
});
|
|
46
|
+
it("accepts a verify block with any_tool_called", () => {
|
|
47
|
+
const result = nodeZ.parse({
|
|
48
|
+
name: "S",
|
|
49
|
+
instruction: "I",
|
|
50
|
+
verify: { any_tool_called: ["linear_create_issue", "github_create_issue"] },
|
|
51
|
+
});
|
|
52
|
+
expect(result.verify?.any_tool_called).toEqual(["linear_create_issue", "github_create_issue"]);
|
|
53
|
+
});
|
|
54
|
+
it("rejects verify with no check declared", () => {
|
|
55
|
+
expect(() => nodeZ.parse({ name: "S", instruction: "I", verify: {} })).toThrow();
|
|
56
|
+
});
|
|
57
|
+
it("rejects verify with empty any_tool_called", () => {
|
|
58
|
+
expect(() => nodeZ.parse({ name: "S", instruction: "I", verify: { any_tool_called: [] } })).toThrow();
|
|
59
|
+
});
|
|
46
60
|
});
|
|
47
61
|
describe("edgeZ", () => {
|
|
48
62
|
it("parses a simple edge", () => {
|
|
@@ -93,7 +93,14 @@ describe("github skill", () => {
|
|
|
93
93
|
describe("linear skill", () => {
|
|
94
94
|
const ctx = () => mockCtx({ LINEAR_API_KEY: "lin_test" });
|
|
95
95
|
it("create_issue sends correct GraphQL mutation", async () => {
|
|
96
|
-
fetchMock.mockResolvedValueOnce(mockResponse({
|
|
96
|
+
fetchMock.mockResolvedValueOnce(mockResponse({
|
|
97
|
+
data: {
|
|
98
|
+
issueCreate: {
|
|
99
|
+
success: true,
|
|
100
|
+
issue: { id: "1", identifier: "TEST-1", url: "https://linear.app/test/issue/TEST-1", title: "Bug" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
97
104
|
const tool = findTool(linear.tools, "linear_create_issue");
|
|
98
105
|
await tool.handler({ teamId: "team1", title: "Bug" }, ctx());
|
|
99
106
|
const [url, opts] = fetchMock.mock.calls[0];
|
|
@@ -102,6 +109,16 @@ describe("linear skill", () => {
|
|
|
102
109
|
const body = JSON.parse(opts.body);
|
|
103
110
|
expect(body.query).toContain("issueCreate");
|
|
104
111
|
});
|
|
112
|
+
it("throws when issueCreate returns no identifier (silent failure guard)", async () => {
|
|
113
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ data: { issueCreate: { success: true, issue: { id: "1" } } } }));
|
|
114
|
+
const tool = findTool(linear.tools, "linear_create_issue");
|
|
115
|
+
await expect(tool.handler({ teamId: "team1", title: "Bug" }, ctx())).rejects.toThrow(/linear_create_issue returned no identifier/);
|
|
116
|
+
});
|
|
117
|
+
it("throws when issueCreate returns success: false", async () => {
|
|
118
|
+
fetchMock.mockResolvedValueOnce(mockResponse({ data: { issueCreate: { success: false } } }));
|
|
119
|
+
const tool = findTool(linear.tools, "linear_create_issue");
|
|
120
|
+
await expect(tool.handler({ teamId: "team1", title: "Bug" }, ctx())).rejects.toThrow(/linear_create_issue returned no identifier/);
|
|
121
|
+
});
|
|
105
122
|
it("search_issues sends correct query", async () => {
|
|
106
123
|
fetchMock.mockResolvedValueOnce(mockResponse({ data: { searchIssues: { nodes: [] } } }));
|
|
107
124
|
const tool = findTool(linear.tools, "linear_search_issues");
|
package/dist/executor.js
CHANGED
|
@@ -121,6 +121,15 @@ export async function execute(workflow, input, options) {
|
|
|
121
121
|
safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
|
|
122
122
|
},
|
|
123
123
|
});
|
|
124
|
+
// Machine-checked post-condition. Enforced AFTER the LLM finishes so that
|
|
125
|
+
// a model claiming success cannot bypass side-effect requirements (e.g.
|
|
126
|
+
// reporting "issue created" when no `linear_create_issue` call was made).
|
|
127
|
+
const verifyError = evaluateVerify(node.verify, result);
|
|
128
|
+
if (verifyError && result.status === "success") {
|
|
129
|
+
result.status = "failed";
|
|
130
|
+
result.data = { ...result.data, error: verifyError };
|
|
131
|
+
logger.warn(` verify failed: ${verifyError}`, { node: currentId });
|
|
132
|
+
}
|
|
124
133
|
results.set(currentId, result);
|
|
125
134
|
trace.steps.push({ node: currentId, status: result.status, iteration });
|
|
126
135
|
safeObserve(observer, { type: "node:exit", node: currentId, result }, logger);
|
|
@@ -155,6 +164,31 @@ export async function execute(workflow, input, options) {
|
|
|
155
164
|
return { results, trace };
|
|
156
165
|
}
|
|
157
166
|
// ─── Internals ───────────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Evaluate a node's `verify` post-condition against the executed result.
|
|
169
|
+
*
|
|
170
|
+
* Returns an error string when the check fails, or null when the node
|
|
171
|
+
* passes verification (or has no verify block).
|
|
172
|
+
*
|
|
173
|
+
* Keep this deterministic and side-effect free — the executor calls it
|
|
174
|
+
* synchronously right after the LLM finishes.
|
|
175
|
+
*/
|
|
176
|
+
function evaluateVerify(verify, result) {
|
|
177
|
+
if (!verify)
|
|
178
|
+
return null;
|
|
179
|
+
if (verify.any_tool_called && verify.any_tool_called.length > 0) {
|
|
180
|
+
const required = new Set(verify.any_tool_called);
|
|
181
|
+
const success = result.toolCalls.some((c) => required.has(c.tool) &&
|
|
182
|
+
// Treat explicit error outputs (from coreToolToSdkTool's catch branch)
|
|
183
|
+
// as failed calls so a swallowed handler throw doesn't count as success.
|
|
184
|
+
!(c.output && typeof c.output === "object" && "error" in c.output));
|
|
185
|
+
if (!success) {
|
|
186
|
+
const called = result.toolCalls.map((c) => c.tool).join(", ") || "none";
|
|
187
|
+
return `verify.any_tool_called failed — required one of [${verify.any_tool_called.join(", ")}] to succeed, but tool calls were: [${called}]`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
158
192
|
/**
|
|
159
193
|
* Build the full instruction for a node.
|
|
160
194
|
*
|
package/dist/schema.d.ts
CHANGED
|
@@ -404,6 +404,17 @@ export declare const nodeSourcesZ: z.ZodUnion<[z.ZodArray<z.ZodUnion<[z.ZodStrin
|
|
|
404
404
|
})[];
|
|
405
405
|
only?: boolean | undefined;
|
|
406
406
|
}>]>;
|
|
407
|
+
export declare const nodeVerifyZ: z.ZodEffects<z.ZodObject<{
|
|
408
|
+
any_tool_called: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
409
|
+
}, "strip", z.ZodTypeAny, {
|
|
410
|
+
any_tool_called?: string[] | undefined;
|
|
411
|
+
}, {
|
|
412
|
+
any_tool_called?: string[] | undefined;
|
|
413
|
+
}>, {
|
|
414
|
+
any_tool_called?: string[] | undefined;
|
|
415
|
+
}, {
|
|
416
|
+
any_tool_called?: string[] | undefined;
|
|
417
|
+
}>;
|
|
407
418
|
export declare const nodeZ: z.ZodObject<{
|
|
408
419
|
name: z.ZodString;
|
|
409
420
|
instruction: z.ZodUnion<[z.ZodString, z.ZodObject<{
|
|
@@ -563,6 +574,17 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
563
574
|
})[];
|
|
564
575
|
only?: boolean | undefined;
|
|
565
576
|
}>]>>;
|
|
577
|
+
verify: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
578
|
+
any_tool_called: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
579
|
+
}, "strip", z.ZodTypeAny, {
|
|
580
|
+
any_tool_called?: string[] | undefined;
|
|
581
|
+
}, {
|
|
582
|
+
any_tool_called?: string[] | undefined;
|
|
583
|
+
}>, {
|
|
584
|
+
any_tool_called?: string[] | undefined;
|
|
585
|
+
}, {
|
|
586
|
+
any_tool_called?: string[] | undefined;
|
|
587
|
+
}>>;
|
|
566
588
|
}, "strip", z.ZodTypeAny, {
|
|
567
589
|
instruction: string | {
|
|
568
590
|
inline: string;
|
|
@@ -612,6 +634,9 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
612
634
|
} | undefined;
|
|
613
635
|
output?: Record<string, unknown> | undefined;
|
|
614
636
|
max_turns?: number | undefined;
|
|
637
|
+
verify?: {
|
|
638
|
+
any_tool_called?: string[] | undefined;
|
|
639
|
+
} | undefined;
|
|
615
640
|
}, {
|
|
616
641
|
instruction: string | {
|
|
617
642
|
inline: string;
|
|
@@ -661,6 +686,9 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
661
686
|
output?: Record<string, unknown> | undefined;
|
|
662
687
|
skills?: string[] | undefined;
|
|
663
688
|
max_turns?: number | undefined;
|
|
689
|
+
verify?: {
|
|
690
|
+
any_tool_called?: string[] | undefined;
|
|
691
|
+
} | undefined;
|
|
664
692
|
}>;
|
|
665
693
|
export declare const edgeZ: z.ZodObject<{
|
|
666
694
|
from: z.ZodString;
|
|
@@ -841,6 +869,17 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
841
869
|
})[];
|
|
842
870
|
only?: boolean | undefined;
|
|
843
871
|
}>]>>;
|
|
872
|
+
verify: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
873
|
+
any_tool_called: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
874
|
+
}, "strip", z.ZodTypeAny, {
|
|
875
|
+
any_tool_called?: string[] | undefined;
|
|
876
|
+
}, {
|
|
877
|
+
any_tool_called?: string[] | undefined;
|
|
878
|
+
}>, {
|
|
879
|
+
any_tool_called?: string[] | undefined;
|
|
880
|
+
}, {
|
|
881
|
+
any_tool_called?: string[] | undefined;
|
|
882
|
+
}>>;
|
|
844
883
|
}, "strip", z.ZodTypeAny, {
|
|
845
884
|
instruction: string | {
|
|
846
885
|
inline: string;
|
|
@@ -890,6 +929,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
890
929
|
} | undefined;
|
|
891
930
|
output?: Record<string, unknown> | undefined;
|
|
892
931
|
max_turns?: number | undefined;
|
|
932
|
+
verify?: {
|
|
933
|
+
any_tool_called?: string[] | undefined;
|
|
934
|
+
} | undefined;
|
|
893
935
|
}, {
|
|
894
936
|
instruction: string | {
|
|
895
937
|
inline: string;
|
|
@@ -939,6 +981,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
939
981
|
output?: Record<string, unknown> | undefined;
|
|
940
982
|
skills?: string[] | undefined;
|
|
941
983
|
max_turns?: number | undefined;
|
|
984
|
+
verify?: {
|
|
985
|
+
any_tool_called?: string[] | undefined;
|
|
986
|
+
} | undefined;
|
|
942
987
|
}>>;
|
|
943
988
|
edges: z.ZodArray<z.ZodObject<{
|
|
944
989
|
from: z.ZodString;
|
|
@@ -1156,6 +1201,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1156
1201
|
} | undefined;
|
|
1157
1202
|
output?: Record<string, unknown> | undefined;
|
|
1158
1203
|
max_turns?: number | undefined;
|
|
1204
|
+
verify?: {
|
|
1205
|
+
any_tool_called?: string[] | undefined;
|
|
1206
|
+
} | undefined;
|
|
1159
1207
|
}>;
|
|
1160
1208
|
edges: {
|
|
1161
1209
|
to: string;
|
|
@@ -1232,6 +1280,9 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
1232
1280
|
output?: Record<string, unknown> | undefined;
|
|
1233
1281
|
skills?: string[] | undefined;
|
|
1234
1282
|
max_turns?: number | undefined;
|
|
1283
|
+
verify?: {
|
|
1284
|
+
any_tool_called?: string[] | undefined;
|
|
1285
|
+
} | undefined;
|
|
1235
1286
|
}>;
|
|
1236
1287
|
edges: {
|
|
1237
1288
|
to: string;
|
|
@@ -1338,6 +1389,9 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
1338
1389
|
} | undefined;
|
|
1339
1390
|
output?: Record<string, unknown> | undefined;
|
|
1340
1391
|
max_turns?: number | undefined;
|
|
1392
|
+
verify?: {
|
|
1393
|
+
any_tool_called?: string[] | undefined;
|
|
1394
|
+
} | undefined;
|
|
1341
1395
|
}>;
|
|
1342
1396
|
edges: {
|
|
1343
1397
|
to: string;
|
package/dist/schema.js
CHANGED
|
@@ -75,6 +75,13 @@ export const nodeSourcesZ = z.union([
|
|
|
75
75
|
sources: z.array(sourceZ),
|
|
76
76
|
}),
|
|
77
77
|
]);
|
|
78
|
+
export const nodeVerifyZ = z
|
|
79
|
+
.object({
|
|
80
|
+
any_tool_called: z.array(z.string().min(1)).min(1).optional(),
|
|
81
|
+
})
|
|
82
|
+
.refine((v) => v.any_tool_called !== undefined, {
|
|
83
|
+
message: "verify must declare at least one check (e.g. any_tool_called)",
|
|
84
|
+
});
|
|
78
85
|
export const nodeZ = z.object({
|
|
79
86
|
name: z.string().min(1),
|
|
80
87
|
instruction: sourceZ,
|
|
@@ -83,6 +90,7 @@ export const nodeZ = z.object({
|
|
|
83
90
|
max_turns: z.number().int().min(1).optional(),
|
|
84
91
|
rules: nodeSourcesZ.optional(),
|
|
85
92
|
context: nodeSourcesZ.optional(),
|
|
93
|
+
verify: nodeVerifyZ.optional(),
|
|
86
94
|
});
|
|
87
95
|
export const edgeZ = z.object({
|
|
88
96
|
from: z.string().min(1),
|
package/dist/skills/linear.js
CHANGED
|
@@ -47,7 +47,13 @@ export const linear = {
|
|
|
47
47
|
},
|
|
48
48
|
required: ["teamId", "title"],
|
|
49
49
|
},
|
|
50
|
-
handler: async (input, ctx) =>
|
|
50
|
+
handler: async (input, ctx) => {
|
|
51
|
+
const data = await linearGql(`mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url title } } }`, { input }, ctx);
|
|
52
|
+
if (!data?.issueCreate?.success || !data?.issueCreate?.issue?.identifier) {
|
|
53
|
+
throw new Error(`[Linear] linear_create_issue returned no identifier — issue was not created. Raw response: ${JSON.stringify(data)}`);
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
},
|
|
51
57
|
},
|
|
52
58
|
{
|
|
53
59
|
name: "linear_search_issues",
|
|
@@ -130,7 +136,11 @@ export const linear = {
|
|
|
130
136
|
},
|
|
131
137
|
handler: async (input, ctx) => {
|
|
132
138
|
const { issueId, ...updates } = input;
|
|
133
|
-
|
|
139
|
+
const data = await linearGql(`mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier url title state { name } } } }`, { id: issueId, input: updates }, ctx);
|
|
140
|
+
if (!data?.issueUpdate?.success) {
|
|
141
|
+
throw new Error(`[Linear] linear_update_issue failed for ${issueId}. Raw response: ${JSON.stringify(data)}`);
|
|
142
|
+
}
|
|
143
|
+
return data;
|
|
134
144
|
},
|
|
135
145
|
},
|
|
136
146
|
],
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,26 @@ export type NodeSources = _Source[] | {
|
|
|
49
49
|
only?: boolean;
|
|
50
50
|
sources: _Source[];
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Machine-checked post-condition for a node.
|
|
54
|
+
*
|
|
55
|
+
* Evaluated by the executor AFTER the LLM finishes. If the check fails, the
|
|
56
|
+
* node is marked `failed` even if the LLM itself returned success — this
|
|
57
|
+
* catches the common case of the model claiming success without actually
|
|
58
|
+
* invoking the side-effectful tool (e.g. reporting "issue created" when no
|
|
59
|
+
* `linear_create_issue` call was made, or the call errored).
|
|
60
|
+
*
|
|
61
|
+
* Keep the shape small and declarative. Anything richer should be a skill
|
|
62
|
+
* or a dedicated workflow node, not a verify clause.
|
|
63
|
+
*/
|
|
64
|
+
export interface NodeVerify {
|
|
65
|
+
/**
|
|
66
|
+
* At least one of the named tools must have been invoked successfully during
|
|
67
|
+
* this node's execution (no `output.error` recorded in `toolCalls`). The
|
|
68
|
+
* node is marked failed otherwise.
|
|
69
|
+
*/
|
|
70
|
+
any_tool_called?: string[];
|
|
71
|
+
}
|
|
52
72
|
/** A node in the workflow DAG */
|
|
53
73
|
export interface Node {
|
|
54
74
|
/** Human-readable name */
|
|
@@ -65,6 +85,8 @@ export interface Node {
|
|
|
65
85
|
rules?: NodeSources;
|
|
66
86
|
/** Per-node background knowledge. Additive by default; set `{ only: true, sources: [...] }` to block cascade. */
|
|
67
87
|
context?: NodeSources;
|
|
88
|
+
/** Machine-checked post-conditions. Enforced by the executor after the LLM finishes. */
|
|
89
|
+
verify?: NodeVerify;
|
|
68
90
|
}
|
|
69
91
|
/** An edge connecting two nodes */
|
|
70
92
|
export interface Edge {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const triageWorkflow = {"id":"triage","name":"Alert Triage","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","nodes":{"gather":{"name":"Gather Context","instruction":"You are investigating a production alert. Gather all relevant context using the available tools:\n\n1. **Observability**: Pull error details, stack traces, recent logs, metrics, and active incidents around the time of the alert. Use whichever observability tools are available to you.\n2. **Source control**: Check recent commits, pull requests, and deploys that might be related.\n3. **Issue tracker**: Search for similar past issues or known problems.\n\nBe thorough — the investigation step depends on complete context. Use every tool available to you.","skills":["github","sentry","datadog","betterstack","linear"]},"investigate":{"name":"Root Cause Analysis","instruction":"Based on the gathered context, classify every distinct issue you found into one of two buckets: **novel** or **duplicate**.\n\nFor EACH issue found:\n1. Identify the root cause and affected code/service.\n2. Assess severity: critical (service down), high (major feature broken), medium (degraded), low (cosmetic/minor).\n3. Assess fix complexity: \"simple\" (a few lines, clear change), \"moderate\" (multiple files but well-understood), or \"complex\" (architectural, risky, or unclear).\n4. **Novelty check (REQUIRED):** Filing a duplicate ticket is worse than under-reporting — the team inherits sprawl that humans have to clean up later. Complete EVERY step below before classifying anything as novel.\n4a. **Search broadly.** Run multiple queries against the issue tracker (BOTH open AND closed): the affected file or module path, the error message or log string verbatim, the feature or service name, and the symptom family (e.g. \"startup crash\", \"data dropped silently\"). Do not rely on a single keyword — a narrow search is how duplicates slip through.\n4b. **Read the full body of every promising candidate — do NOT match on title alone.** For each top match, open the issue and read its entire description, including any \"Recommended Fix\", \"Proposed Fix\", \"Action Items\", or \"Acceptance Criteria\" section. An existing open issue whose recommended fix already prescribes the change you were about to propose IS a duplicate, even if the symptom you just observed is a new instance of the same underlying bug.\n4c. **Check for meta-issues and bug families.** If an existing ticket names a pattern that your finding is another instance of (same root cause, same function/module, different input or caller), treat your finding as a sibling of that family and comment on the existing ticket. Do not file a parallel ticket per instance — that is the exact pattern that produces backlog sprawl.\n4d. **When uncertain, comment don't file.** If you are less than ~80% confident a finding is truly novel after steps 4a–4c, classify it as a duplicate and add a \"+1 — another instance\" comment to the closest existing issue with the new context. Over-reporting duplicates is cheap; ticket sprawl is expensive.\n\n - If matched → **duplicate**. Record the existing issue ID in `duplicate_of`.\n - If no match after 4a–4d → **novel**.\n\n**Output rules:**\n- `findings`: array of ALL issues found (both novel and duplicate).\n- `novel_count`: how many findings are novel (not duplicates).\n- `highest_severity`: the highest severity across ALL findings.\n- `recommendation`: what should happen next.\n\nDownstream nodes will act ONLY on novel findings. Duplicates will be +1'd automatically.","skills":["github","linear"],"output":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","description":"Short description of the issue"},"root_cause":{"type":"string"},"severity":{"type":"string","enum":["critical","high","medium","low"]},"affected_services":{"type":"array","items":{"type":"string"}},"is_duplicate":{"type":"boolean"},"duplicate_of":{"type":"string","description":"Existing issue ID/URL if duplicate"},"fix_approach":{"type":"string"},"fix_complexity":{"type":"string","enum":["simple","moderate","complex"]}},"required":["title","root_cause","severity","is_duplicate"]}},"novel_count":{"type":"number","description":"Count of novel (non-duplicate) findings"},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","highest_severity","recommendation"]}},"create_issue":{"name":"Create Issues & Triage Duplicates","instruction":"Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.\n\n**For each NOVEL finding** (is_duplicate = false):\n1. Create a new issue with a clear, actionable title.\n2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n3. Add appropriate labels (bug, severity level, affected service).\n4. Link to relevant commits, PRs, or existing issues.\n\n**For each DUPLICATE finding** (is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. Check the issue's comments — if the most recent comment is already from SWEny (contains \"+1\") within the last 24 hours, skip adding another comment.\n3. Otherwise add a SHORT comment: \"+1 — seen again {UTC timestamp}. {one sentence of new context}.\" (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)\n4. If the existing issue is closed/done, reopen it.\n\nIf context.issueTemplate is provided, use it as the format for new issue bodies. Otherwise use a clear structure with: Summary, Root Cause, Impact, Steps to Reproduce, and Recommended Fix.\n\nUse the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues) — do NOT fall back to a different tracker. Output the created/updated issue identifiers.","skills":["linear","github"],"output":{"type":"object","properties":{"issueIdentifier":{"type":"string","description":"Primary issue identifier (e.g. OFF-1234). This is the first novel issue created — used by downstream nodes for branch naming and PR linking."},"issueTitle":{"type":"string","description":"Title of the primary issue created"},"issueUrl":{"type":"string","description":"URL of the primary issue created"},"issues":{"type":"array","description":"All issues created or updated during this step","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"action":{"type":"string","enum":["created","updated","reopened"]}},"required":["identifier","action"]}}},"required":["issueIdentifier","issueTitle","issueUrl","issues"]}},"skip":{"name":"Skip — All Duplicates or Low Priority","instruction":"Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.\n\nFor each **duplicate** finding (check the findings array for items where is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. Check the issue's comments — if the most recent comment is already from SWEny (contains \"+1\") within the last 24 hours, skip adding another comment.\n3. Otherwise add a SHORT comment: \"+1 — seen again {UTC timestamp}. {one sentence of new context}.\" (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)\n4. If the issue is closed/done, reopen it.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation. The issue created in the previous step is in your context — use its identifier for branch naming and commit messages.\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n **Branch name MUST include the issue identifier** (e.g. context.create_issue.issueIdentifier).\n Format: lowercase the identifier and use it as a prefix, e.g. `off-1234-fix-null-check`.\n If the repository uses a different branch convention (check context.branchPattern), follow that instead.\n2. Read the relevant source files to understand the current code.\n3. Make the necessary code changes — fix the bug, nothing more.\n4. Run any existing tests if available to verify the fix doesn't break anything.\n5. Stage and commit with a clear commit message. Start the message with the issue identifier in brackets,\n e.g. `[OFF-1234] fix: guard null projectId before database lookup`.\n\nKeep changes minimal and focused. Do not refactor surrounding code or add unrelated improvements.\n\nIf the fix turns out to be more complex than expected, stop and explain why — do not force a bad fix.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix. The issue identifier and URL are in your context from the create_issue step.\n\n1. Push the branch to the remote.\n2. Create a PR with a title that starts with the issue identifier in brackets. Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.\n3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue (context.create_issue.issueUrl). If the investigation found a Sentry issue, include \"Fixes {sentryShortId}\" in the PR body so Sentry auto-resolves on merge.\n4. Add appropriate labels if available.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL and number.","skills":["github"]},"notify":{"name":"Notify Team","instruction":"Send a notification summarizing the triage result:\n\n1. Include: alert summary, severity, root cause (1-2 sentences), and links to any created issues or PRs.\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"]}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":"novel_count is greater than 0 AND highest_severity is medium or higher"},{"from":"investigate","to":"skip","when":"novel_count is 0, OR highest_severity is low"},{"from":"create_issue","to":"implement","when":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","when":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"create_pr"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
1
|
+
export const triageWorkflow = {"id":"triage","name":"Alert Triage","description":"Investigate a production alert, determine root cause, create an issue, implement a fix, and notify the team","entry":"gather","nodes":{"gather":{"name":"Gather Context","instruction":"You are investigating a production alert. Gather all relevant context using the available tools:\n\n1. **Observability**: Pull error details, stack traces, recent logs, metrics, and active incidents around the time of the alert. Use whichever observability tools are available to you.\n2. **Source control**: Check recent commits, pull requests, and deploys that might be related.\n3. **Issue tracker**: Search for similar past issues or known problems.\n\nBe thorough — the investigation step depends on complete context. Use every tool available to you.","skills":["github","sentry","datadog","betterstack","linear"]},"investigate":{"name":"Root Cause Analysis","instruction":"Based on the gathered context, classify every distinct issue you found into one of two buckets: **novel** or **duplicate**.\n\nFor EACH issue found:\n1. Identify the root cause and affected code/service.\n2. Assess severity: critical (service down), high (major feature broken), medium (degraded), low (cosmetic/minor).\n3. Assess fix complexity: \"simple\" (a few lines, clear change), \"moderate\" (multiple files but well-understood), or \"complex\" (architectural, risky, or unclear).\n4. **Novelty check (REQUIRED):** Filing a duplicate ticket is worse than under-reporting — the team inherits sprawl that humans have to clean up later. Complete EVERY step below before classifying anything as novel.\n4a. **Search broadly.** Run multiple queries against the issue tracker (BOTH open AND closed): the affected file or module path, the error message or log string verbatim, the feature or service name, and the symptom family (e.g. \"startup crash\", \"data dropped silently\"). Do not rely on a single keyword — a narrow search is how duplicates slip through.\n4b. **Read the full body of every promising candidate — do NOT match on title alone.** For each top match, open the issue and read its entire description, including any \"Recommended Fix\", \"Proposed Fix\", \"Action Items\", or \"Acceptance Criteria\" section. An existing open issue whose recommended fix already prescribes the change you were about to propose IS a duplicate, even if the symptom you just observed is a new instance of the same underlying bug.\n4c. **Check for meta-issues and bug families.** If an existing ticket names a pattern that your finding is another instance of (same root cause, same function/module, different input or caller), treat your finding as a sibling of that family and comment on the existing ticket. Do not file a parallel ticket per instance — that is the exact pattern that produces backlog sprawl.\n4d. **When uncertain, comment don't file.** If you are less than ~80% confident a finding is truly novel after steps 4a–4c, classify it as a duplicate and add a \"+1 — another instance\" comment to the closest existing issue with the new context. Over-reporting duplicates is cheap; ticket sprawl is expensive.\n\n - If matched → **duplicate**. Record the existing issue ID in `duplicate_of`.\n - If no match after 4a–4d → **novel**.\n\n**Output rules:**\n- `findings`: array of ALL issues found (both novel and duplicate).\n- `novel_count`: how many findings are novel (not duplicates).\n- `highest_severity`: the highest severity across ALL findings.\n- `recommendation`: what should happen next.\n\nDownstream nodes will act ONLY on novel findings. Duplicates will be +1'd automatically.","skills":["github","linear"],"output":{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","description":"Short description of the issue"},"root_cause":{"type":"string"},"severity":{"type":"string","enum":["critical","high","medium","low"]},"affected_services":{"type":"array","items":{"type":"string"}},"is_duplicate":{"type":"boolean"},"duplicate_of":{"type":"string","description":"Existing issue ID/URL if duplicate"},"fix_approach":{"type":"string"},"fix_complexity":{"type":"string","enum":["simple","moderate","complex"]}},"required":["title","root_cause","severity","is_duplicate"]}},"novel_count":{"type":"number","description":"Count of novel (non-duplicate) findings"},"highest_severity":{"type":"string","enum":["critical","high","medium","low"]},"recommendation":{"type":"string"}},"required":["findings","novel_count","highest_severity","recommendation"]}},"create_issue":{"name":"Create Issues & Triage Duplicates","instruction":"Process ALL findings from the investigation. The findings array contains both novel and duplicate issues.\n\n**For each NOVEL finding** (is_duplicate = false):\n1. Create a new issue with a clear, actionable title by invoking the appropriate tool (`linear_create_issue` or `github_create_issue`). You MUST actually call the tool — do NOT synthesize an identifier from an alert ID, commit SHA, or your own imagination. If the tool errors, stop and report the failure; do not invent a fallback identifier.\n2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.\n3. Add appropriate labels (bug, severity level, affected service).\n4. Link to relevant commits, PRs, or existing issues.\n5. Copy the identifier returned by the tool verbatim into the `issueIdentifier` output field. The identifier must match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`) — do not prefix it with anything else.\n\n**For each DUPLICATE finding** (is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. Check the issue's comments — if the most recent comment is already from SWEny (contains \"+1\") within the last 24 hours, skip adding another comment.\n3. Otherwise add a SHORT comment: \"+1 — seen again {UTC timestamp}. {one sentence of new context}.\" (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)\n4. If the existing issue is closed/done, reopen it.\n\nIf context.issueTemplate is provided, use it as the format for new issue bodies. Otherwise use a clear structure with: Summary, Root Cause, Impact, Steps to Reproduce, and Recommended Fix.\n\nUse the issue tracker specified in context (e.g. Linear, Jira, or GitHub Issues) — do NOT fall back to a different tracker. Output the created/updated issue identifiers.","skills":["linear","github"],"verify":{"any_tool_called":["linear_create_issue","github_create_issue"]},"output":{"type":"object","properties":{"issueIdentifier":{"type":"string","pattern":"^[A-Z][A-Z0-9]*-\\d+$","description":"Primary issue identifier (e.g. OFF-1234). MUST be the identifier returned by `linear_create_issue` / `github_create_issue` — do NOT invent a value from a Sentry short-ID or commit SHA. Used by downstream nodes for branch naming and PR linking."},"issueTitle":{"type":"string","description":"Title of the primary issue created"},"issueUrl":{"type":"string","pattern":"^https?://","description":"URL of the primary issue created, as returned by the create-issue tool"},"issues":{"type":"array","minItems":1,"description":"All issues created or updated during this step","items":{"type":"object","properties":{"identifier":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"action":{"type":"string","enum":["created","updated","reopened"]}},"required":["identifier","action"]}}},"required":["issueIdentifier","issueTitle","issueUrl","issues"]}},"skip":{"name":"Skip — All Duplicates or Low Priority","instruction":"Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.\n\nFor each **duplicate** finding (check the findings array for items where is_duplicate = true):\n1. Find the existing issue (check duplicate_of field).\n2. Check the issue's comments — if the most recent comment is already from SWEny (contains \"+1\") within the last 24 hours, skip adding another comment.\n3. Otherwise add a SHORT comment: \"+1 — seen again {UTC timestamp}. {one sentence of new context}.\" (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)\n4. If the issue is closed/done, reopen it.\n\nFor **low priority** findings, log a brief note about why they were skipped.","skills":["linear","github"]},"implement":{"name":"Implement Fix","instruction":"Implement the fix identified during investigation. The issue created in the previous step is in your context — use its identifier for branch naming and commit messages.\n\n0. **Precondition — verify context.create_issue.issueIdentifier is well-formed.**\n The identifier MUST match `^[A-Z][A-Z0-9]*-\\d+$` (e.g. `OFF-1234`). If it is\n missing, empty, or matches a Sentry short-ID / slug / any other shape, STOP\n immediately — do NOT invent a branch name from the Sentry event, commit SHA,\n or alert payload. Return a failed status with a clear message instead; the\n workflow will route to notify and a human will pick it up.\n\n1. Create a feature branch from the base branch (check context for baseBranch, default \"main\").\n **Branch name MUST include the issue identifier** exactly as provided in\n `context.create_issue.issueIdentifier`. Format: lowercase the identifier and use\n it as a prefix, e.g. `off-1234-fix-null-check`. If the repository uses a different\n branch convention (check context.branchPattern or the rules/CLAUDE.md for a\n regex), follow that — but the identifier piece MUST still come from\n `context.create_issue.issueIdentifier`, never from a Sentry short-ID.\n2. Read the relevant source files to understand the current code.\n3. Make the necessary code changes — fix the bug, nothing more.\n4. Run any existing tests if available to verify the fix doesn't break anything.\n5. Stage and commit with a clear commit message. Start the message with the issue identifier in brackets,\n e.g. `[OFF-1234] fix: guard null projectId before database lookup`.\n\nKeep changes minimal and focused. Do not refactor surrounding code or add unrelated improvements.\n\nIf the fix turns out to be more complex than expected, stop and explain why — do not force a bad fix.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix. The issue identifier and URL are in your context from the create_issue step.\n\n1. Push the branch to the remote.\n2. Create a PR with a title that starts with the issue identifier in brackets. Format: `[{context.create_issue.issueIdentifier}] fix: {short description}`.\n3. In the PR body, include: summary of the bug, what the fix does, and a link to the issue (context.create_issue.issueUrl). If the investigation found a Sentry issue, include \"Fixes {sentryShortId}\" in the PR body so Sentry auto-resolves on merge.\n4. Add appropriate labels if available.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL and number.","skills":["github"]},"notify":{"name":"Notify Team","instruction":"Send a notification summarizing the triage result:\n\n1. Include: alert summary, severity, root cause (1-2 sentences), and links to any created issues or PRs.\n2. For critical/high severity, make the notification urgent.\n3. For medium/low, a standard notification is fine.\n\nUse whichever notification channel is available to you.","skills":["slack","notification"]}},"edges":[{"from":"gather","to":"investigate"},{"from":"investigate","to":"create_issue","when":"novel_count is greater than 0 AND highest_severity is medium or higher"},{"from":"investigate","to":"skip","when":"novel_count is 0, OR highest_severity is low"},{"from":"create_issue","to":"implement","when":"at least one novel finding has fix_complexity simple or moderate AND fix_approach is provided"},{"from":"create_issue","to":"notify","when":"all novel findings have fix_complexity complex, OR no clear fix_approach"},{"from":"skip","to":"notify"},{"from":"implement","to":"create_pr"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
2
2
|
export const implementWorkflow = {"id":"implement","name":"Implement Fix","description":"Implement a code fix for an issue and open a pull request","entry":"analyze","nodes":{"analyze":{"name":"Analyze Issue","instruction":"Read the issue details and understand what needs to be fixed:\n\n1. Fetch the issue from the tracker (GitHub or Linear).\n2. Read the relevant source files to understand the current code.\n3. Identify the exact files and lines that need to change.\n4. Plan the fix approach.\n\nOutput a clear analysis of what needs to change and why.","skills":["github","linear"],"output":{"type":"object","properties":{"issue_summary":{"type":"string"},"files_to_change":{"type":"array","items":{"type":"string"}},"fix_plan":{"type":"string"},"risk_level":{"type":"string","enum":["low","medium","high"]}},"required":["issue_summary","fix_plan"]}},"implement":{"name":"Implement Fix","instruction":"Implement the planned fix:\n\n1. Create a feature branch.\n2. Make the necessary code changes.\n3. Ensure changes are minimal and focused — fix the bug, nothing more.\n4. Stage and commit with a clear commit message referencing the issue.\n\nIf the fix is too risky or complex, explain why and skip.","skills":["github"]},"create_pr":{"name":"Open Pull Request","instruction":"Open a pull request for the fix:\n\n1. Push the branch to the remote.\n2. Create a PR with a clear title and description.\n3. Reference the original issue in the PR body.\n4. Add appropriate reviewers or labels if possible.\n\nIf context.prTemplate is provided, use it as the format for the PR body. Otherwise use a clear structure with: Summary, Changes, Testing, and Related Issues.\n\nIf context.cloudRunId is provided, append this HTML comment to the end of the PR body: <!-- sweny:run_id:{context.cloudRunId} -->\n\nReturn the PR URL.","skills":["github"]},"notify":{"name":"Notify","instruction":"Send a notification about the implementation result:\n\n1. Include: issue reference, PR link, brief summary of changes.\n2. Keep it concise — one message, not a wall of text.","skills":["slack","notification"]},"skip":{"name":"Skip — Too Complex","instruction":"The fix was determined to be too complex or risky for automated implementation.\nAdd a comment to the issue explaining what was found and why it needs manual attention.","skills":["github","linear"]}},"edges":[{"from":"analyze","to":"implement","when":"Fix risk level is low or medium and a clear plan exists"},{"from":"analyze","to":"skip","when":"Fix is too complex, risky, or unclear"},{"from":"implement","to":"create_pr"},{"from":"skip","to":"notify"},{"from":"create_pr","to":"notify"}],"skills":{}};
|
|
3
3
|
export const seedContentWorkflow = {"id":"seed-content","name":"Content Generation Pipeline","description":"Audit content gaps, generate educational content, validate quality, and publish to Supabase","entry":"audit","nodes":{"audit":{"name":"Audit Content Gaps","instruction":"You are auditing an educational platform's content library.\n\nUse supabase_count to check how many items exist for each combination of:\n- Content types: worksheets, games, lessons\n- Subjects: math, science, english, history, geography, economics, music, art, life-skills, foreign-language\n- Difficulties: beginner, easy, medium, hard\n\nAlso check the input parameters — if the user specified a subject, topic, difficulty, and count,\nskip the full audit and just report what they want generated.\n\nReturn a structured content plan: what to generate, how many, at what difficulty and grade level.\nPrioritize gaps where a subject has ZERO content of a given type.","skills":["supabase"],"output":{"type":"object","properties":{"plan":{"type":"array","items":{"type":"object","properties":{"contentType":{"type":"string"},"subject":{"type":"string"},"topic":{"type":"string"},"difficulty":{"type":"string"},"gradeLevel":{"type":"string"},"count":{"type":"number"},"reason":{"type":"string"}},"required":["contentType","subject","topic","difficulty","count"]}},"summary":{"type":"string"}},"required":["plan","summary"]}},"generate":{"name":"Generate Content","instruction":"You are an expert educator creating content for children ages 5-14.\n\nFor each item in the content plan from the previous step, generate the content directly.\nThe content must be high-quality, engaging, age-appropriate, and educationally sound.\n\n**For worksheets:**\nGenerate a JSON array of problems. Each problem needs: \"problem\", \"answer\", \"format\" (answer-line | multiple-choice | write-on).\nRequirements:\n- Every answer MUST be correct. Double-check math, facts, spelling.\n- Problems must progress in difficulty within the set.\n- Use at least 2 different problem structures (e.g., equations + word problems for math).\n- Vary phrasing — no two problems should feel copy-pasted.\n- For the target grade level, use appropriate vocabulary and complexity.\n\n**For lessons:**\nGenerate complete HTML + CSS + JS for an interactive lesson.\nRequirements:\n- Must have substantial educational content (not just a shell with navigation).\n- Include interactive elements: quizzes, click-to-reveal, drag-and-drop.\n- Must be visually engaging with colors, icons, and clear typography.\n- Include practice exercises with immediate feedback.\n- Minimum 5 teaching sections plus 2 practice activities.\n\n**For games:**\nGenerate a description and educational requirements only (actual game code is generated\nby the platform's Phaser-based generator).\n\nUse supabase_insert to write each piece of content to the appropriate table.\n\nTable schemas:\n- worksheets: id (auto), title, subject, topic, difficulty, grade_level, num_questions, problems (jsonb), status='published', source='ai-seed'\n- lessons: id (auto), title, subject, difficulty, grade_level, lesson_type, html_code, css_code, js_code, status='published', source='ai-seed'\n- games: id (auto), title, subject, difficulty, grade_level, game_type, html_code, css_code, js_code, status='published', source='ai-seed'\n\nReturn a summary of what was generated and inserted.","skills":["supabase"],"output":{"type":"object","properties":{"generated":{"type":"array","items":{"type":"object","properties":{"contentType":{"type":"string"},"title":{"type":"string"},"subject":{"type":"string"},"id":{"type":"string"},"status":{"type":"string"}}}},"totalGenerated":{"type":"number"},"errors":{"type":"array","items":{"type":"string"}}},"required":["generated","totalGenerated"]}},"validate":{"name":"Validate Content Quality","instruction":"Review the content that was just generated and inserted.\n\nFor each item, use supabase_query to fetch it back and verify:\n\n**Worksheets:**\n- Parse the problems JSON — are all answers correct?\n- Are problems diverse (not repetitive)?\n- Is difficulty appropriate for the stated grade level?\n- Are there the right number of questions?\n\n**Lessons:**\n- Is the HTML substantial (> 500 characters)?\n- Does it contain actual educational content (not just navigation shells)?\n- Are there interactive elements?\n\n**Games:**\n- Does the HTML contain a working game structure?\n\nFlag any issues. If critical issues are found (wrong answers, empty content),\nuse supabase_update to set status='draft' and add a note.\n\nReturn a quality report.","skills":["supabase"],"output":{"type":"object","properties":{"totalReviewed":{"type":"number"},"passed":{"type":"number"},"flagged":{"type":"number"},"issues":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"issue":{"type":"string"},"severity":{"type":"string"},"action":{"type":"string"}}}},"summary":{"type":"string"}},"required":["totalReviewed","passed","flagged","summary"]}},"report":{"name":"Generate Report","instruction":"Compile a final report of the content generation run.\n\nInclude:\n- Total content generated by type and subject\n- Quality validation results\n- Any issues found and actions taken\n- Recommendations for next content generation run (what gaps remain)\n\nFormat as a clean markdown summary that a solo operator can scan in 30 seconds.","skills":[],"output":{"type":"object","properties":{"markdown":{"type":"string"},"nextPriorities":{"type":"array","items":{"type":"string"}}},"required":["markdown"]}}},"edges":[{"from":"audit","to":"generate"},{"from":"generate","to":"validate"},{"from":"validate","to":"report"}],"skills":{}};
|
|
@@ -148,7 +148,10 @@ nodes:
|
|
|
148
148
|
|
|
149
149
|
**For each NOVEL finding** (is_duplicate = false):
|
|
150
150
|
|
|
151
|
-
1. Create a new issue with a clear, actionable title
|
|
151
|
+
1. Create a new issue with a clear, actionable title by invoking the appropriate tool
|
|
152
|
+
(`linear_create_issue` or `github_create_issue`). You MUST actually call the tool — do NOT
|
|
153
|
+
synthesize an identifier from an alert ID, commit SHA, or your own imagination. If the
|
|
154
|
+
tool errors, stop and report the failure; do not invent a fallback identifier.
|
|
152
155
|
|
|
153
156
|
2. Include: root cause, severity, affected services, reproduction steps, and recommended fix.
|
|
154
157
|
|
|
@@ -156,6 +159,10 @@ nodes:
|
|
|
156
159
|
|
|
157
160
|
4. Link to relevant commits, PRs, or existing issues.
|
|
158
161
|
|
|
162
|
+
5. Copy the identifier returned by the tool verbatim into the `issueIdentifier` output
|
|
163
|
+
field. The identifier must match `^[A-Z][A-Z0-9]*-\d+$` (e.g. `OFF-1234`) — do not
|
|
164
|
+
prefix it with anything else.
|
|
165
|
+
|
|
159
166
|
|
|
160
167
|
**For each DUPLICATE finding** (is_duplicate = true):
|
|
161
168
|
|
|
@@ -179,20 +186,35 @@ nodes:
|
|
|
179
186
|
skills:
|
|
180
187
|
- linear
|
|
181
188
|
- github
|
|
189
|
+
verify:
|
|
190
|
+
# Reject output that claims success without a real issue-creation tool call.
|
|
191
|
+
# The executor checks this after the node finishes; if none of these tools
|
|
192
|
+
# was invoked successfully, the node is marked failed and downstream nodes
|
|
193
|
+
# (implement, create_pr) never run with a hallucinated identifier.
|
|
194
|
+
any_tool_called:
|
|
195
|
+
- linear_create_issue
|
|
196
|
+
- github_create_issue
|
|
182
197
|
output:
|
|
183
198
|
type: object
|
|
184
199
|
properties:
|
|
185
200
|
issueIdentifier:
|
|
186
201
|
type: string
|
|
187
|
-
|
|
202
|
+
pattern: '^[A-Z][A-Z0-9]*-\d+$'
|
|
203
|
+
description: >-
|
|
204
|
+
Primary issue identifier (e.g. OFF-1234). MUST be the identifier returned by
|
|
205
|
+
`linear_create_issue` / `github_create_issue` — do NOT invent a value from a
|
|
206
|
+
Sentry short-ID or commit SHA. Used by downstream nodes for branch naming and
|
|
207
|
+
PR linking.
|
|
188
208
|
issueTitle:
|
|
189
209
|
type: string
|
|
190
210
|
description: Title of the primary issue created
|
|
191
211
|
issueUrl:
|
|
192
212
|
type: string
|
|
193
|
-
|
|
213
|
+
pattern: "^https?://"
|
|
214
|
+
description: URL of the primary issue created, as returned by the create-issue tool
|
|
194
215
|
issues:
|
|
195
216
|
type: array
|
|
217
|
+
minItems: 1
|
|
196
218
|
description: All issues created or updated during this step
|
|
197
219
|
items:
|
|
198
220
|
type: object
|
|
@@ -245,10 +267,20 @@ nodes:
|
|
|
245
267
|
instruction: |-
|
|
246
268
|
Implement the fix identified during investigation. The issue created in the previous step is in your context — use its identifier for branch naming and commit messages.
|
|
247
269
|
|
|
270
|
+
0. **Precondition — verify context.create_issue.issueIdentifier is well-formed.**
|
|
271
|
+
The identifier MUST match `^[A-Z][A-Z0-9]*-\d+$` (e.g. `OFF-1234`). If it is
|
|
272
|
+
missing, empty, or matches a Sentry short-ID / slug / any other shape, STOP
|
|
273
|
+
immediately — do NOT invent a branch name from the Sentry event, commit SHA,
|
|
274
|
+
or alert payload. Return a failed status with a clear message instead; the
|
|
275
|
+
workflow will route to notify and a human will pick it up.
|
|
276
|
+
|
|
248
277
|
1. Create a feature branch from the base branch (check context for baseBranch, default "main").
|
|
249
|
-
**Branch name MUST include the issue identifier**
|
|
250
|
-
Format: lowercase the identifier and use
|
|
251
|
-
If the repository uses a different
|
|
278
|
+
**Branch name MUST include the issue identifier** exactly as provided in
|
|
279
|
+
`context.create_issue.issueIdentifier`. Format: lowercase the identifier and use
|
|
280
|
+
it as a prefix, e.g. `off-1234-fix-null-check`. If the repository uses a different
|
|
281
|
+
branch convention (check context.branchPattern or the rules/CLAUDE.md for a
|
|
282
|
+
regex), follow that — but the identifier piece MUST still come from
|
|
283
|
+
`context.create_issue.issueIdentifier`, never from a Sentry short-ID.
|
|
252
284
|
2. Read the relevant source files to understand the current code.
|
|
253
285
|
3. Make the necessary code changes — fix the bug, nothing more.
|
|
254
286
|
4. Run any existing tests if available to verify the fix doesn't break anything.
|