@sweny-ai/core 0.1.0

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.
Files changed (93) hide show
  1. package/dist/__tests__/claude.test.d.ts +1 -0
  2. package/dist/__tests__/claude.test.js +328 -0
  3. package/dist/__tests__/executor.test.d.ts +1 -0
  4. package/dist/__tests__/executor.test.js +296 -0
  5. package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration/datadog.integration.test.js +23 -0
  7. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
  8. package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
  9. package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
  10. package/dist/__tests__/integration/github.integration.test.js +37 -0
  11. package/dist/__tests__/integration/harness.d.ts +24 -0
  12. package/dist/__tests__/integration/harness.js +34 -0
  13. package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
  14. package/dist/__tests__/integration/linear.integration.test.js +15 -0
  15. package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
  16. package/dist/__tests__/integration/sentry.integration.test.js +20 -0
  17. package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
  18. package/dist/__tests__/integration/slack.integration.test.js +22 -0
  19. package/dist/__tests__/schema.test.d.ts +1 -0
  20. package/dist/__tests__/schema.test.js +239 -0
  21. package/dist/__tests__/skills-index.test.d.ts +1 -0
  22. package/dist/__tests__/skills-index.test.js +122 -0
  23. package/dist/__tests__/skills.test.d.ts +1 -0
  24. package/dist/__tests__/skills.test.js +296 -0
  25. package/dist/__tests__/studio.test.d.ts +1 -0
  26. package/dist/__tests__/studio.test.js +172 -0
  27. package/dist/__tests__/testing.test.d.ts +1 -0
  28. package/dist/__tests__/testing.test.js +224 -0
  29. package/dist/browser.d.ts +17 -0
  30. package/dist/browser.js +22 -0
  31. package/dist/claude.d.ts +48 -0
  32. package/dist/claude.js +293 -0
  33. package/dist/cli/check.d.ts +11 -0
  34. package/dist/cli/check.js +237 -0
  35. package/dist/cli/config-file.d.ts +12 -0
  36. package/dist/cli/config-file.js +208 -0
  37. package/dist/cli/config.d.ts +77 -0
  38. package/dist/cli/config.js +565 -0
  39. package/dist/cli/main.d.ts +10 -0
  40. package/dist/cli/main.js +744 -0
  41. package/dist/cli/output.d.ts +26 -0
  42. package/dist/cli/output.js +357 -0
  43. package/dist/cli/renderer.d.ts +33 -0
  44. package/dist/cli/renderer.js +423 -0
  45. package/dist/cli/renderer.test.d.ts +1 -0
  46. package/dist/cli/renderer.test.js +302 -0
  47. package/dist/cli/setup.d.ts +11 -0
  48. package/dist/cli/setup.js +310 -0
  49. package/dist/executor.d.ts +29 -0
  50. package/dist/executor.js +173 -0
  51. package/dist/executor.test.d.ts +1 -0
  52. package/dist/executor.test.js +314 -0
  53. package/dist/index.d.ts +37 -0
  54. package/dist/index.js +36 -0
  55. package/dist/mcp.d.ts +11 -0
  56. package/dist/mcp.js +183 -0
  57. package/dist/mcp.test.d.ts +1 -0
  58. package/dist/mcp.test.js +334 -0
  59. package/dist/schema.d.ts +318 -0
  60. package/dist/schema.js +207 -0
  61. package/dist/skills/betterstack.d.ts +7 -0
  62. package/dist/skills/betterstack.js +114 -0
  63. package/dist/skills/datadog.d.ts +7 -0
  64. package/dist/skills/datadog.js +107 -0
  65. package/dist/skills/github.d.ts +8 -0
  66. package/dist/skills/github.js +155 -0
  67. package/dist/skills/index.d.ts +68 -0
  68. package/dist/skills/index.js +134 -0
  69. package/dist/skills/linear.d.ts +7 -0
  70. package/dist/skills/linear.js +89 -0
  71. package/dist/skills/notification.d.ts +11 -0
  72. package/dist/skills/notification.js +142 -0
  73. package/dist/skills/sentry.d.ts +7 -0
  74. package/dist/skills/sentry.js +105 -0
  75. package/dist/skills/slack.d.ts +8 -0
  76. package/dist/skills/slack.js +115 -0
  77. package/dist/studio.d.ts +124 -0
  78. package/dist/studio.js +174 -0
  79. package/dist/testing.d.ts +88 -0
  80. package/dist/testing.js +253 -0
  81. package/dist/types.d.ts +144 -0
  82. package/dist/types.js +11 -0
  83. package/dist/workflow-builder.d.ts +45 -0
  84. package/dist/workflow-builder.js +120 -0
  85. package/dist/workflow-builder.test.d.ts +1 -0
  86. package/dist/workflow-builder.test.js +117 -0
  87. package/dist/workflows/implement.d.ts +11 -0
  88. package/dist/workflows/implement.js +83 -0
  89. package/dist/workflows/index.d.ts +2 -0
  90. package/dist/workflows/index.js +2 -0
  91. package/dist/workflows/triage.d.ts +18 -0
  92. package/dist/workflows/triage.js +108 -0
  93. package/package.json +83 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Test Helpers
3
+ *
4
+ * MockClaude for running workflows without an API key.
5
+ * File-based skill for local testing.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { MockClaude, fileSkill } from '@sweny-ai/core/testing'
10
+ *
11
+ * const claude = new MockClaude({
12
+ * gather: {
13
+ * toolCalls: [{ tool: 'fs_read_json', input: { path: 'logs.json' } }],
14
+ * data: { logs: [...] },
15
+ * },
16
+ * investigate: {
17
+ * data: { root_cause: 'NPE in handler', severity: 'high' },
18
+ * },
19
+ * })
20
+ * ```
21
+ */
22
+ import type { Claude, NodeResult, Tool, JSONSchema, Workflow } from "./types.js";
23
+ export interface MockNodeResponse {
24
+ /** Tool calls to execute (optional — handlers will be called) */
25
+ toolCalls?: {
26
+ tool: string;
27
+ input: Record<string, unknown>;
28
+ }[];
29
+ /** Data to return as the node result */
30
+ data?: Record<string, unknown>;
31
+ /** Status (default: "success") */
32
+ status?: "success" | "skipped" | "failed";
33
+ }
34
+ export interface MockClaudeOptions {
35
+ /** Node ID → scripted response */
36
+ responses: Record<string, MockNodeResponse>;
37
+ /** Route decisions: "fromNode" → chosen target node ID */
38
+ routes?: Record<string, string>;
39
+ /** Workflow definition — enables instruction-based node matching (required for branching workflows) */
40
+ workflow?: Workflow;
41
+ }
42
+ /**
43
+ * A mock Claude client that follows a script.
44
+ *
45
+ * For each node, it executes scripted tool calls and returns
46
+ * scripted results. For routing decisions, it follows the
47
+ * routes map or defaults to the first choice.
48
+ */
49
+ export declare class MockClaude implements Claude {
50
+ private callOrder;
51
+ private responses;
52
+ private routes;
53
+ private instructionMap;
54
+ constructor(opts: MockClaudeOptions);
55
+ /** Returns the order in which nodes were executed */
56
+ get executedNodes(): string[];
57
+ run(opts: {
58
+ instruction: string;
59
+ context: Record<string, unknown>;
60
+ tools: Tool[];
61
+ outputSchema?: JSONSchema;
62
+ }): Promise<NodeResult>;
63
+ evaluate(opts: {
64
+ question: string;
65
+ context: Record<string, unknown>;
66
+ choices: {
67
+ id: string;
68
+ description: string;
69
+ }[];
70
+ }): Promise<string>;
71
+ /**
72
+ * Identify which node is being executed.
73
+ *
74
+ * Strategy:
75
+ * 1. If a workflow was provided, match by instruction text (accurate for branching)
76
+ * 2. Otherwise, fall back to sequential key matching (works for linear DAGs)
77
+ */
78
+ private identifyNode;
79
+ }
80
+ import type { Skill } from "./types.js";
81
+ /**
82
+ * Create a file-based skill for local testing.
83
+ *
84
+ * Replaces ALL four file providers (observability, issue-tracking,
85
+ * source-control, notification) with a single skill that reads/writes
86
+ * local JSON/markdown files.
87
+ */
88
+ export declare function createFileSkill(outputDir: string): Skill;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Test Helpers
3
+ *
4
+ * MockClaude for running workflows without an API key.
5
+ * File-based skill for local testing.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { MockClaude, fileSkill } from '@sweny-ai/core/testing'
10
+ *
11
+ * const claude = new MockClaude({
12
+ * gather: {
13
+ * toolCalls: [{ tool: 'fs_read_json', input: { path: 'logs.json' } }],
14
+ * data: { logs: [...] },
15
+ * },
16
+ * investigate: {
17
+ * data: { root_cause: 'NPE in handler', severity: 'high' },
18
+ * },
19
+ * })
20
+ * ```
21
+ */
22
+ import { consoleLogger } from "./types.js";
23
+ /**
24
+ * A mock Claude client that follows a script.
25
+ *
26
+ * For each node, it executes scripted tool calls and returns
27
+ * scripted results. For routing decisions, it follows the
28
+ * routes map or defaults to the first choice.
29
+ */
30
+ export class MockClaude {
31
+ callOrder = [];
32
+ responses;
33
+ routes;
34
+ instructionMap; // instruction text → node ID
35
+ constructor(opts) {
36
+ this.responses = opts.responses;
37
+ this.routes = opts.routes ?? {};
38
+ // Build reverse map: instruction → node ID (for accurate matching in branching workflows)
39
+ this.instructionMap = new Map();
40
+ if (opts.workflow) {
41
+ for (const [id, node] of Object.entries(opts.workflow.nodes)) {
42
+ this.instructionMap.set(node.instruction, id);
43
+ }
44
+ }
45
+ }
46
+ /** Returns the order in which nodes were executed */
47
+ get executedNodes() {
48
+ return [...this.callOrder];
49
+ }
50
+ async run(opts) {
51
+ // Identify which node this is by matching instruction text against responses
52
+ // The executor wraps tools with event tracking, so we can find the node
53
+ // by checking which response key's instruction appears
54
+ const nodeId = this.identifyNode(opts.instruction);
55
+ this.callOrder.push(nodeId);
56
+ const response = this.responses[nodeId];
57
+ if (!response) {
58
+ return {
59
+ status: "success",
60
+ data: { summary: `Mock: no scripted response for "${nodeId}"` },
61
+ toolCalls: [],
62
+ };
63
+ }
64
+ // Execute scripted tool calls
65
+ const toolCalls = [];
66
+ if (response.toolCalls) {
67
+ for (const tc of response.toolCalls) {
68
+ const tool = opts.tools.find((t) => t.name === tc.tool);
69
+ if (tool) {
70
+ const defaultCtx = { config: {}, logger: consoleLogger };
71
+ const output = await tool.handler(tc.input, defaultCtx);
72
+ toolCalls.push({ tool: tc.tool, input: tc.input, output });
73
+ }
74
+ else {
75
+ toolCalls.push({ tool: tc.tool, input: tc.input, output: { error: "tool not found" } });
76
+ }
77
+ }
78
+ }
79
+ return {
80
+ status: response.status ?? "success",
81
+ data: response.data ?? {},
82
+ toolCalls,
83
+ };
84
+ }
85
+ async evaluate(opts) {
86
+ // Check if we have a scripted route from the last executed node
87
+ const lastNode = this.callOrder[this.callOrder.length - 1];
88
+ if (lastNode && this.routes[lastNode]) {
89
+ const route = this.routes[lastNode];
90
+ // Validate route is a valid choice
91
+ if (opts.choices.some((c) => c.id === route)) {
92
+ return route;
93
+ }
94
+ }
95
+ // Default: first choice
96
+ return opts.choices[0].id;
97
+ }
98
+ /**
99
+ * Identify which node is being executed.
100
+ *
101
+ * Strategy:
102
+ * 1. If a workflow was provided, match by instruction text (accurate for branching)
103
+ * 2. Otherwise, fall back to sequential key matching (works for linear DAGs)
104
+ */
105
+ identifyNode(instruction) {
106
+ // 1. Instruction-based matching (accurate for branching workflows)
107
+ if (this.instructionMap.size > 0) {
108
+ const nodeId = this.instructionMap.get(instruction);
109
+ if (nodeId && nodeId in this.responses)
110
+ return nodeId;
111
+ }
112
+ // 2. Check if any response key appears literally in the instruction
113
+ const keys = Object.keys(this.responses);
114
+ for (const key of keys) {
115
+ if (instruction.toLowerCase().includes(key.toLowerCase()) && !this.callOrder.includes(key)) {
116
+ return key;
117
+ }
118
+ }
119
+ // 3. Sequential fallback: return the next unused key
120
+ const unused = keys.filter((k) => !this.callOrder.includes(k));
121
+ return unused[0] ?? `unknown-${this.callOrder.length}`;
122
+ }
123
+ }
124
+ /**
125
+ * Create a file-based skill for local testing.
126
+ *
127
+ * Replaces ALL four file providers (observability, issue-tracking,
128
+ * source-control, notification) with a single skill that reads/writes
129
+ * local JSON/markdown files.
130
+ */
131
+ export function createFileSkill(outputDir) {
132
+ // Lazy-loaded — only resolved when a handler actually runs (Node-only)
133
+ let _fs = null;
134
+ let _path = null;
135
+ let _resolved = null;
136
+ async function getFs() {
137
+ if (!_fs)
138
+ _fs = await import("node:fs");
139
+ return _fs;
140
+ }
141
+ async function getResolved() {
142
+ if (!_path)
143
+ _path = await import("node:path");
144
+ if (!_resolved)
145
+ _resolved = _path.resolve(outputDir);
146
+ return { path: _path, resolved: _resolved };
147
+ }
148
+ return {
149
+ id: "filesystem",
150
+ name: "Local Filesystem",
151
+ category: "general",
152
+ description: "Read logs and write issues/PRs/notifications to local files (for testing)",
153
+ config: {},
154
+ tools: [
155
+ {
156
+ name: "fs_read_json",
157
+ description: "Read and parse a JSON file",
158
+ input_schema: {
159
+ type: "object",
160
+ properties: {
161
+ path: { type: "string", description: "File path (absolute or relative to output dir)" },
162
+ },
163
+ required: ["path"],
164
+ },
165
+ handler: async (input) => {
166
+ const fs = await getFs();
167
+ const { path: p, resolved } = await getResolved();
168
+ const filePath = p.isAbsolute(input.path) ? input.path : p.join(resolved, input.path);
169
+ const raw = fs.readFileSync(filePath, "utf-8");
170
+ return JSON.parse(raw);
171
+ },
172
+ },
173
+ {
174
+ name: "fs_read_text",
175
+ description: "Read a text file",
176
+ input_schema: {
177
+ type: "object",
178
+ properties: {
179
+ path: { type: "string" },
180
+ },
181
+ required: ["path"],
182
+ },
183
+ handler: async (input) => {
184
+ const fs = await getFs();
185
+ const { path: p, resolved } = await getResolved();
186
+ const filePath = p.isAbsolute(input.path) ? input.path : p.join(resolved, input.path);
187
+ return fs.readFileSync(filePath, "utf-8");
188
+ },
189
+ },
190
+ {
191
+ name: "fs_write_json",
192
+ description: "Write a JSON object to a file",
193
+ input_schema: {
194
+ type: "object",
195
+ properties: {
196
+ path: { type: "string", description: "File path relative to output dir" },
197
+ data: { type: "object", description: "JSON data to write" },
198
+ },
199
+ required: ["path", "data"],
200
+ },
201
+ handler: async (input) => {
202
+ const fs = await getFs();
203
+ const { path: p, resolved } = await getResolved();
204
+ const filePath = p.join(resolved, input.path);
205
+ fs.mkdirSync(p.dirname(filePath), { recursive: true });
206
+ fs.writeFileSync(filePath, JSON.stringify(input.data, null, 2), "utf-8");
207
+ return { written: filePath };
208
+ },
209
+ },
210
+ {
211
+ name: "fs_write_markdown",
212
+ description: "Write a markdown file (for issues, PRs, notifications)",
213
+ input_schema: {
214
+ type: "object",
215
+ properties: {
216
+ path: { type: "string", description: "File path relative to output dir" },
217
+ content: { type: "string", description: "Markdown content" },
218
+ },
219
+ required: ["path", "content"],
220
+ },
221
+ handler: async (input) => {
222
+ const fs = await getFs();
223
+ const { path: p, resolved } = await getResolved();
224
+ const filePath = p.join(resolved, input.path);
225
+ fs.mkdirSync(p.dirname(filePath), { recursive: true });
226
+ fs.writeFileSync(filePath, input.content, "utf-8");
227
+ return { written: filePath };
228
+ },
229
+ },
230
+ {
231
+ name: "fs_list_dir",
232
+ description: "List files in a directory",
233
+ input_schema: {
234
+ type: "object",
235
+ properties: {
236
+ path: { type: "string", description: "Directory path relative to output dir" },
237
+ },
238
+ },
239
+ handler: async (input) => {
240
+ const fs = await getFs();
241
+ const { path: p, resolved } = await getResolved();
242
+ const dirPath = input.path ? p.join(resolved, input.path) : resolved;
243
+ try {
244
+ return fs.readdirSync(dirPath);
245
+ }
246
+ catch (err) {
247
+ return { error: `Failed to list directory: ${err.message}`, files: [] };
248
+ }
249
+ },
250
+ },
251
+ ],
252
+ };
253
+ }
@@ -0,0 +1,144 @@
1
+ export type JSONSchema = Record<string, unknown>;
2
+ /** Context passed to tool handlers at execution time */
3
+ export interface ToolContext {
4
+ /** Resolved config values (env vars + explicit overrides) */
5
+ config: Record<string, string>;
6
+ /** Structured logger */
7
+ logger: Logger;
8
+ }
9
+ /** A single tool Claude can invoke */
10
+ export interface Tool {
11
+ name: string;
12
+ description: string;
13
+ input_schema: JSONSchema;
14
+ handler: (input: any, ctx: ToolContext) => Promise<unknown>;
15
+ }
16
+ /** Config field declaration — skills say what they need */
17
+ export interface ConfigField {
18
+ description: string;
19
+ required?: boolean;
20
+ /** Default environment variable to read from */
21
+ env?: string;
22
+ }
23
+ /** Skill categories — used for validation and grouping */
24
+ export type SkillCategory = "git" | "observability" | "tasks" | "notification" | "general";
25
+ /** A skill groups related tools with shared config requirements */
26
+ export interface Skill {
27
+ id: string;
28
+ name: string;
29
+ description: string;
30
+ category: SkillCategory;
31
+ config: Record<string, ConfigField>;
32
+ tools: Tool[];
33
+ }
34
+ /** A node in the workflow DAG */
35
+ export interface Node {
36
+ /** Human-readable name */
37
+ name: string;
38
+ /** What Claude should accomplish at this step */
39
+ instruction: string;
40
+ /** Skill IDs available at this node */
41
+ skills: string[];
42
+ /** Optional structured output schema */
43
+ output?: JSONSchema;
44
+ }
45
+ /** An edge connecting two nodes */
46
+ export interface Edge {
47
+ from: string;
48
+ to: string;
49
+ /** Natural language condition — Claude evaluates at runtime */
50
+ when?: string;
51
+ }
52
+ /** A complete workflow definition — pure data, fully serializable */
53
+ export interface Workflow {
54
+ id: string;
55
+ name: string;
56
+ description: string;
57
+ nodes: Record<string, Node>;
58
+ edges: Edge[];
59
+ entry: string;
60
+ }
61
+ export interface NodeResult {
62
+ status: "success" | "skipped" | "failed";
63
+ /** Arbitrary data produced by this node */
64
+ data: Record<string, unknown>;
65
+ /** Tool calls made during this node's execution */
66
+ toolCalls: ToolCall[];
67
+ }
68
+ export interface ToolCall {
69
+ tool: string;
70
+ input: unknown;
71
+ output: unknown;
72
+ }
73
+ export type ExecutionEvent = {
74
+ type: "workflow:start";
75
+ workflow: string;
76
+ } | {
77
+ type: "node:enter";
78
+ node: string;
79
+ instruction: string;
80
+ } | {
81
+ type: "tool:call";
82
+ node: string;
83
+ tool: string;
84
+ input: unknown;
85
+ } | {
86
+ type: "tool:result";
87
+ node: string;
88
+ tool: string;
89
+ output: unknown;
90
+ } | {
91
+ type: "node:exit";
92
+ node: string;
93
+ result: NodeResult;
94
+ } | {
95
+ type: "route";
96
+ from: string;
97
+ to: string;
98
+ reason: string;
99
+ } | {
100
+ type: "workflow:end";
101
+ results: Record<string, NodeResult>;
102
+ };
103
+ export type Observer = (event: ExecutionEvent) => void;
104
+ export interface Claude {
105
+ /** Run a node: give Claude an instruction, context, and tools */
106
+ run(opts: {
107
+ instruction: string;
108
+ context: Record<string, unknown>;
109
+ tools: Tool[];
110
+ outputSchema?: JSONSchema;
111
+ }): Promise<NodeResult>;
112
+ /** Evaluate a routing condition — pick one of N choices */
113
+ evaluate(opts: {
114
+ question: string;
115
+ context: Record<string, unknown>;
116
+ choices: {
117
+ id: string;
118
+ description: string;
119
+ }[];
120
+ }): Promise<string>;
121
+ }
122
+ export interface McpServerConfig {
123
+ type: "stdio" | "http";
124
+ command?: string;
125
+ args?: string[];
126
+ url?: string;
127
+ headers?: Record<string, string>;
128
+ env?: Record<string, string>;
129
+ }
130
+ export interface McpAutoConfig {
131
+ sourceControlProvider?: string;
132
+ issueTrackerProvider?: string;
133
+ observabilityProvider?: string;
134
+ credentials: Record<string, string>;
135
+ workspaceTools?: string[];
136
+ userMcpServers?: Record<string, McpServerConfig>;
137
+ }
138
+ export interface Logger {
139
+ info(msg: string, data?: Record<string, unknown>): void;
140
+ warn(msg: string, data?: Record<string, unknown>): void;
141
+ error(msg: string, data?: Record<string, unknown>): void;
142
+ debug(msg: string, data?: Record<string, unknown>): void;
143
+ }
144
+ export declare const consoleLogger: Logger;
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ // ─── Skill System ────────────────────────────────────────────────
2
+ //
3
+ // A Skill is a logical group of tools that share configuration.
4
+ // Skills replace "providers" — instead of typed interfaces that
5
+ // step code calls, skills expose tools that Claude calls directly.
6
+ export const consoleLogger = {
7
+ info: (msg, data) => console.log(`[info] ${msg}`, data ?? ""),
8
+ warn: (msg, data) => console.warn(`[warn] ${msg}`, data ?? ""),
9
+ error: (msg, data) => console.error(`[error] ${msg}`, data ?? ""),
10
+ debug: (msg, data) => console.debug(`[debug] ${msg}`, data ?? ""),
11
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Workflow Builder
3
+ *
4
+ * Generates and refines SWEny workflow definitions using the Claude
5
+ * interface (headless Claude Code). Accepts a natural language
6
+ * description and available skills, and returns a validated Workflow.
7
+ */
8
+ import type { Workflow, Skill, Claude, Logger } from "./types.js";
9
+ export interface BuildWorkflowOptions {
10
+ /** Claude client (headless Claude Code) */
11
+ claude: Claude;
12
+ /** Skills available for use in the workflow */
13
+ skills: Skill[];
14
+ /** Optional logger */
15
+ logger?: Logger;
16
+ }
17
+ /**
18
+ * Build the system prompt sent to Claude as the instruction.
19
+ * Includes the JSON schema, available skills, instruction quality
20
+ * guidance, rules, and optionally an existing workflow to refine.
21
+ */
22
+ export declare function buildSystemPrompt(skills: Skill[], existingWorkflow?: Workflow): string;
23
+ /**
24
+ * Extract and validate a Workflow from the raw data returned by
25
+ * claude.run(). The ClaudeClient spreads parsed JSON into data and
26
+ * adds a `summary` key from the text response. We strip `summary`
27
+ * and any other non-workflow keys before parsing.
28
+ */
29
+ export declare function extractWorkflow(data: Record<string, unknown>): Workflow;
30
+ /**
31
+ * Generate a complete workflow from a natural language description.
32
+ *
33
+ * Builds a system prompt with the workflow JSON schema, available
34
+ * skills, instruction quality guidance, and rules. Calls claude.run()
35
+ * with no tools (pure generation task). Parses and validates the
36
+ * returned JSON as a Workflow.
37
+ */
38
+ export declare function buildWorkflow(description: string, options: BuildWorkflowOptions): Promise<Workflow>;
39
+ /**
40
+ * Refine an existing workflow based on a natural language instruction.
41
+ *
42
+ * Same as buildWorkflow but includes the current workflow in the
43
+ * prompt as context so Claude can modify it.
44
+ */
45
+ export declare function refineWorkflow(workflow: Workflow, instruction: string, options: BuildWorkflowOptions): Promise<Workflow>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Workflow Builder
3
+ *
4
+ * Generates and refines SWEny workflow definitions using the Claude
5
+ * interface (headless Claude Code). Accepts a natural language
6
+ * description and available skills, and returns a validated Workflow.
7
+ */
8
+ import { workflowZ, validateWorkflow, workflowJsonSchema } from "./schema.js";
9
+ // ─── Instruction quality guidance ────────────────────────────────
10
+ const INSTRUCTION_GUIDANCE = `Each node's \`instruction\` field is a detailed prompt that Claude will execute autonomously.
11
+ Write instructions as if briefing a skilled engineer who has access to the node's tools
12
+ but no other context. Be specific about:
13
+
14
+ - WHAT to query/search/create (not just "check for errors" — specify filters, time ranges, grouping)
15
+ - HOW to interpret results (what counts as actionable? what thresholds matter?)
16
+ - WHAT output to produce (structured findings, not just "summarize")
17
+ - HOW to handle edge cases (no results found, too many results, ambiguous data)
18
+
19
+ Bad: "Query Sentry for errors"
20
+ Good: "Query Sentry for unresolved errors from the last 24 hours. Group by issue
21
+ fingerprint. For each group, note: error count, affected services, first/last
22
+ seen timestamps, and stack trace summary. Prioritize by frequency × recency.
23
+ If no errors found, report that explicitly so downstream nodes can skip."`;
24
+ // ─── Helpers ─────────────────────────────────────────────────────
25
+ /**
26
+ * Build the system prompt sent to Claude as the instruction.
27
+ * Includes the JSON schema, available skills, instruction quality
28
+ * guidance, rules, and optionally an existing workflow to refine.
29
+ */
30
+ export function buildSystemPrompt(skills, existingWorkflow) {
31
+ const skillList = skills.map((s) => `- ${s.id}: ${s.description}`).join("\n");
32
+ const parts = [
33
+ "You generate SWEny workflow definitions as JSON.",
34
+ "",
35
+ "## Workflow JSON Schema",
36
+ "```json",
37
+ JSON.stringify(workflowJsonSchema, null, 2),
38
+ "```",
39
+ "",
40
+ "## Available Skills",
41
+ skillList || "(none)",
42
+ "",
43
+ "## Instruction Quality",
44
+ INSTRUCTION_GUIDANCE,
45
+ "",
46
+ "## Rules",
47
+ "- Use snake_case for node IDs (e.g. gather_errors, create_ticket)",
48
+ "- Set `entry` to the first node in the flow",
49
+ "- Only reference skill IDs from the list above in node `skills` arrays",
50
+ "- Use natural language for edge `when` conditions",
51
+ "- Every node must be reachable from the entry node",
52
+ "- Return ONLY the workflow JSON object — no markdown fences, no explanation",
53
+ ];
54
+ if (existingWorkflow) {
55
+ parts.push("", "## Current Workflow (modify this)", "```json", JSON.stringify(existingWorkflow, null, 2), "```");
56
+ }
57
+ return parts.join("\n");
58
+ }
59
+ /**
60
+ * Extract and validate a Workflow from the raw data returned by
61
+ * claude.run(). The ClaudeClient spreads parsed JSON into data and
62
+ * adds a `summary` key from the text response. We strip `summary`
63
+ * and any other non-workflow keys before parsing.
64
+ */
65
+ export function extractWorkflow(data) {
66
+ // Remove keys added by ClaudeClient that aren't part of the workflow schema
67
+ const { summary: _summary, ...rest } = data;
68
+ // workflowZ.parse throws a ZodError on invalid input
69
+ const parsed = workflowZ.parse(rest);
70
+ const errors = validateWorkflow(parsed);
71
+ if (errors.length > 0) {
72
+ throw new Error(`Generated workflow has validation errors: ${errors.map((e) => e.message).join("; ")}`);
73
+ }
74
+ return parsed;
75
+ }
76
+ // ─── Public API ───────────────────────────────────────────────────
77
+ /**
78
+ * Generate a complete workflow from a natural language description.
79
+ *
80
+ * Builds a system prompt with the workflow JSON schema, available
81
+ * skills, instruction quality guidance, and rules. Calls claude.run()
82
+ * with no tools (pure generation task). Parses and validates the
83
+ * returned JSON as a Workflow.
84
+ */
85
+ export async function buildWorkflow(description, options) {
86
+ const { claude, skills, logger } = options;
87
+ const instruction = buildSystemPrompt(skills);
88
+ logger?.debug("buildWorkflow: calling claude.run", { description });
89
+ const result = await claude.run({
90
+ instruction,
91
+ context: { description },
92
+ tools: [],
93
+ outputSchema: workflowJsonSchema,
94
+ });
95
+ if (result.status === "failed") {
96
+ throw new Error(`Claude failed to generate workflow: ${result.data.error ?? "unknown error"}`);
97
+ }
98
+ return extractWorkflow(result.data);
99
+ }
100
+ /**
101
+ * Refine an existing workflow based on a natural language instruction.
102
+ *
103
+ * Same as buildWorkflow but includes the current workflow in the
104
+ * prompt as context so Claude can modify it.
105
+ */
106
+ export async function refineWorkflow(workflow, instruction, options) {
107
+ const { claude, skills, logger } = options;
108
+ const systemPrompt = buildSystemPrompt(skills, workflow);
109
+ logger?.debug("refineWorkflow: calling claude.run", { instruction });
110
+ const result = await claude.run({
111
+ instruction: systemPrompt,
112
+ context: { instruction },
113
+ tools: [],
114
+ outputSchema: workflowJsonSchema,
115
+ });
116
+ if (result.status === "failed") {
117
+ throw new Error(`Claude failed to refine workflow: ${result.data.error ?? "unknown error"}`);
118
+ }
119
+ return extractWorkflow(result.data);
120
+ }
@@ -0,0 +1 @@
1
+ export {};