@zibby/workflow-templates 0.2.1 → 0.4.1

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 (38) hide show
  1. package/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  2. package/browser-test-automation/package.json +1 -0
  3. package/code-analysis/graph.js +5 -4
  4. package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  5. package/code-analysis/nodes/analyze-ticket-node.js +9 -2
  6. package/code-analysis/nodes/generate-code-node.js +27 -11
  7. package/code-analysis/nodes/setup-node.js +50 -130
  8. package/code-analysis/nodes/utils/get-repo-path.js +32 -0
  9. package/code-analysis/prompts/setup.md +71 -0
  10. package/code-analysis/state.js +16 -0
  11. package/generate-test-cases/graph.mjs +11 -1
  12. package/generate-test-cases/nodes/setup-node.js +32 -130
  13. package/generate-test-cases/prompts/setup.md +50 -0
  14. package/generate-test-cases/state.js +12 -0
  15. package/index.js +136 -0
  16. package/notify-lark/README.md +88 -0
  17. package/notify-lark/graph.mjs +43 -0
  18. package/notify-lark/icon.png +0 -0
  19. package/notify-lark/nodes/notify-lark-node.js +290 -0
  20. package/notify-lark/package.json +18 -0
  21. package/notify-lark/state.js +75 -0
  22. package/notify-slack/README.md +94 -0
  23. package/notify-slack/graph.mjs +51 -0
  24. package/notify-slack/icon.png +0 -0
  25. package/notify-slack/nodes/notify-slack-node.js +238 -0
  26. package/notify-slack/package.json +18 -0
  27. package/notify-slack/state.js +93 -0
  28. package/package.json +12 -3
  29. package/sentry-triage/graph.mjs +81 -0
  30. package/sentry-triage/icon.png +0 -0
  31. package/sentry-triage/nodes/classify-node.js +38 -0
  32. package/sentry-triage/nodes/dispatch-alerts-node.js +191 -0
  33. package/sentry-triage/nodes/fetch-issues-node.js +52 -0
  34. package/sentry-triage/nodes/filter-noise-node.js +112 -0
  35. package/sentry-triage/package.json +18 -0
  36. package/sentry-triage/prompts/classify.md +76 -0
  37. package/sentry-triage/prompts/fetch-issues.md +66 -0
  38. package/sentry-triage/state.js +134 -0
@@ -0,0 +1 @@
1
+ {"version":"4.1.5","results":[[":__tests__/preflight-early-exit.test.mjs",{"duration":6.5747499999999945,"failed":false}]]}
@@ -7,6 +7,7 @@
7
7
  "main": "graph.mjs",
8
8
  "dependencies": {
9
9
  "@zibby/core": "^0.5.1",
10
+ "@zibby/ui-memory": "^1.0.0",
10
11
  "zod": "^3.23.0"
11
12
  }
12
13
  }
@@ -21,17 +21,18 @@ function loadPrompt(filename) {
21
21
  }
22
22
 
23
23
  // Load prompt templates at graph definition time
24
+ const setupPrompt = loadPrompt('setup.md');
24
25
  const analyzeTicketPrompt = loadPrompt('analyze-ticket.md');
25
26
  const generateCodePrompt = loadPrompt('generate-code.md');
26
27
  const generateTestCasesPrompt = loadPrompt('generate-test-cases.md');
27
28
 
28
29
  export function buildAnalysisGraph(graph) {
29
30
  graph.setStateSchema(analysisStateSchema);
30
-
31
+
31
32
  graph
32
- .addNode('setup', setupNode)
33
- .addNode('analyze_ticket', analyzeTicketNode, {
34
- prompt: analyzeTicketPrompt
33
+ .addNode('setup', setupNode, { prompt: setupPrompt })
34
+ .addNode('analyze_ticket', analyzeTicketNode, {
35
+ prompt: analyzeTicketPrompt,
35
36
  })
36
37
  .addConditionalNode('validation_check', {
37
38
  condition: (state) => {
@@ -0,0 +1 @@
1
+ {"version":"4.1.5","results":[[":nodes/__tests__/middleware.integration.test.js",{"duration":0,"failed":true}],[":nodes/__tests__/finalizeNode.test.js",{"duration":8.396791000000007,"failed":false}]]}
@@ -1,8 +1,9 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { randomBytes } from 'crypto';
4
- import { z } from 'zod';
4
+ import { z, SKILLS } from '@zibby/core';
5
5
  import { adfToText } from '@zibby/core/utils/adf-converter.js';
6
+ import { getRepoPath } from './utils/get-repo-path.js';
6
7
 
7
8
  const generateId = () => randomBytes(16).toString('hex');
8
9
 
@@ -85,6 +86,12 @@ const AnalyzeTicketNodeOutputSchema = z.object({
85
86
 
86
87
  export const analyzeTicketNode = {
87
88
  name: 'analyze_ticket',
89
+ // skills:['jira'] makes the LLM aware of jira_search / jira_get_issue
90
+ // tools (handy when the ticketContext only carries a key and the agent
91
+ // needs to pull the full issue). Also drives the marketplace card's
92
+ // "Required Integrations: Jira" badge so users connect Jira before
93
+ // installing this workflow.
94
+ skills: [SKILLS.JIRA],
88
95
  outputSchema: AnalyzeTicketNodeOutputSchema,
89
96
 
90
97
  execute: async (context) => {
@@ -116,7 +123,7 @@ export const analyzeTicketNode = {
116
123
  additionalContext: ticketContext.additionalContext || null,
117
124
  repositories: Array.isArray(repos) ? repos.map(r => ({
118
125
  name: r.name,
119
- ...detectProjectInfo(join(workspace, r.name))
126
+ ...detectProjectInfo(getRepoPath(state, r.name))
120
127
  })) : []
121
128
  };
122
129
 
@@ -5,14 +5,24 @@
5
5
  */
6
6
 
7
7
  import { spawn } from 'child_process';
8
- import { join, resolve } from 'path';
8
+ import { dirname, join, resolve } from 'path';
9
9
  import { existsSync, readFileSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
10
11
  import Handlebars from 'handlebars';
11
12
  import { invokeAgent } from '@zibby/core';
12
13
  import { generatePRMeta } from './services/prMetaService.js';
13
14
  import { adfToText } from '@zibby/core/utils/adf-converter.js';
15
+ import { getRepoPath } from './utils/get-repo-path.js';
14
16
  import { z } from 'zod';
15
17
 
18
+ // Prompts ship inside the workflow bundle at `<template>/prompts/`.
19
+ // state.promptsDir is the runner-injection slot for callers that want
20
+ // to override (legacy analyze-graph CLI set it explicitly); when unset,
21
+ // resolve relative to this node file so the template is self-contained
22
+ // and works for any runner.
23
+ const __nodeDir = dirname(fileURLToPath(import.meta.url));
24
+ const DEFAULT_PROMPTS_DIR = join(__nodeDir, '..', 'prompts');
25
+
16
26
  const CodeImplementationOutputSchema = z.object({
17
27
  success: z.boolean(),
18
28
  codeImplementation: z.object({
@@ -59,19 +69,22 @@ export function createCodeGenerationNode(options = {}) {
59
69
  const mode = commitAndPush ? 'implementing' : 'generating preview of';
60
70
  console.log(`\n💻 ${commitAndPush ? 'Implementing' : 'Generating'} code implementation...`);
61
71
 
62
- const { workspace, ticketContext, repos, promptsDir, model, nodeConfigs = {} } = state;
72
+ const { workspace, ticketContext, repos, model, nodeConfigs = {} } = state;
73
+ const promptsDir = state.promptsDir || DEFAULT_PROMPTS_DIR;
63
74
  const aiModel = model || ticketContext.model || 'auto';
64
75
  const _nodeConfig = nodeConfigs[nodeName] || {};
65
76
  const analysis = state.analyze_ticket?.analysis;
66
77
 
67
- // Build implementation prompt from template
78
+ // Build implementation prompt from template. Pass `state` so the
79
+ // prompt builder can resolve cloned repo paths from state.setup
80
+ // (populated by the LLM-driven setup node).
68
81
  const implementPrompt = buildImplementationPrompt(
69
82
  promptsDir,
70
83
  ticketContext,
71
84
  analysis,
72
85
  commitAndPush,
73
86
  repos,
74
- workspace
87
+ state
75
88
  );
76
89
 
77
90
  console.log(`🚀 Running AI Agent to ${mode} changes with model: ${aiModel}...`);
@@ -96,7 +109,7 @@ export function createCodeGenerationNode(options = {}) {
96
109
  commitMessage = `${ticketContext.ticketKey}: ${ticketContext.summary}\n\n${commitDesc}\n\nImplemented by Zibby Agent`;
97
110
 
98
111
  for (const repo of reposToCommit) {
99
- const repoPath = join(workspace, repo.name);
112
+ const repoPath = getRepoPath(state, repo.name);
100
113
 
101
114
  // Check if this repo has any changes
102
115
  const status = await execCommand('git', ['status', '--porcelain'], repoPath);
@@ -153,9 +166,9 @@ export function createCodeGenerationNode(options = {}) {
153
166
  : [{ name: '.' }];
154
167
 
155
168
  for (const repo of reposToCheck) {
156
- const currentRepoPath = repo.name === '.'
157
- ? workspace
158
- : join(workspace, repo.name);
169
+ const currentRepoPath = repo.name === '.'
170
+ ? workspace
171
+ : getRepoPath(state, repo.name);
159
172
 
160
173
  try {
161
174
  let repoDiff, repoDiffStat, repoChangedFiles;
@@ -314,7 +327,8 @@ export const implementCodeNode = createCodeGenerationNode({
314
327
  nodeName: 'implement_code'
315
328
  });
316
329
 
317
- function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, workspace) {
330
+ function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, state) {
331
+ const workspace = state?.workspace;
318
332
  const templatePath = join(promptsDir, 'generate-code.md');
319
333
  if (!existsSync(templatePath)) {
320
334
  throw new Error(`Template not found: ${templatePath}`);
@@ -342,9 +356,11 @@ function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommit
342
356
  }
343
357
  }
344
358
 
345
- // Build repo metadata for the template
359
+ // Build repo metadata for the template. Use getRepoPath() so we
360
+ // detect language from wherever the setup node actually checked the
361
+ // repo out (state.setup.clonedRepos[].path).
346
362
  const repositories = (repos && repos.length > 0) ? repos.map(r => {
347
- const info = detectRepoLanguage(workspace ? join(workspace, r.name) : null);
363
+ const info = detectRepoLanguage(getRepoPath(state, r.name));
348
364
  return { name: r.name, ...info };
349
365
  }) : null;
350
366
 
@@ -1,142 +1,62 @@
1
1
  /**
2
- * Setup Node - Clone repositories and initialize git baseline
3
- * Used by: analysisGraph, implementationGraph
2
+ * Setup Node — LLM-driven workspace bootstrap.
3
+ *
4
+ * The agent receives the `git` skill (which exposes git_checkout +
5
+ * git_list_repos + git_explore as tools) plus a Bash tool, and is
6
+ * prompted to:
7
+ * 1. Clone each repo in state.repos. git_checkout auto-authenticates
8
+ * with GITHUB_TOKEN / GITLAB_TOKEN injected by the runner — works
9
+ * for both providers without the prompt needing to know which.
10
+ * 2. Initialize a baseline git repo at state.workspace so downstream
11
+ * diff tooling can compute generated changes.
12
+ * 3. Return the structured outputSchema below — downstream nodes read
13
+ * state.setup.clonedRepos[].path to find each repo locally.
14
+ *
15
+ * Why LLM-driven instead of raw `git clone` (the previous design):
16
+ * - User-customizable via prompts/setup.md (no JS edits needed)
17
+ * - Same node handles GitHub + GitLab transparently — the agent picks
18
+ * the right tool based on what's connected
19
+ * - Composable with state.repos changes: add a new repo to the project
20
+ * config and the agent clones it next run, no node code touched
4
21
  */
5
22
 
6
- import { spawn } from 'child_process';
7
- import { join } from 'path';
8
- import { z } from 'zod';
23
+ import { readFileSync, existsSync } from 'fs';
24
+ import { dirname, join } from 'path';
25
+ import { fileURLToPath } from 'url';
26
+ import { z, SKILLS } from '@zibby/core';
27
+
28
+ // Prompt is loaded at module-import time from prompts/setup.md so users
29
+ // can edit it without touching this file. We attach it inline on the
30
+ // node config (rather than via graph.addNode(..., { prompt })) because
31
+ // the inline form is what other LLM nodes in this codebase use (see
32
+ // browser-test-automation/nodes/execute-live.mjs); same place agents
33
+ // look the prompt up at run time, no nodePrompts map indirection.
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const promptPath = join(__dirname, '..', 'prompts', 'setup.md');
36
+ const setupPrompt = existsSync(promptPath)
37
+ ? readFileSync(promptPath, 'utf-8')
38
+ : '';
9
39
 
10
40
  const SetupOutputSchema = z.object({
11
- success: z.boolean(),
41
+ success: z.boolean().describe('true if all repos cloned + baseline initialized'),
12
42
  clonedRepos: z.array(z.object({
13
- name: z.string(),
14
- path: z.string(),
15
- isPrimary: z.boolean().optional()
16
- })),
17
- baselineCommit: z.string()
43
+ name: z.string().describe('Repo name (matches state.repos[].name)'),
44
+ path: z.string().describe('Absolute local path where the repo was cloned'),
45
+ isPrimary: z.boolean().optional().describe('True for the project\'s primary repo'),
46
+ })).describe('Every repo from state.repos must appear here once'),
47
+ baselineCommit: z.string().describe('git rev-parse HEAD at state.workspace after baseline commit'),
18
48
  });
19
49
 
20
50
  export const setupNode = {
21
51
  name: 'setup',
52
+ // SKILLS.GIT gives the LLM git_checkout / git_list_repos / git_explore.
53
+ // The skill auto-auths against whichever provider is connected
54
+ // (GitHub OR GitLab) — having either one satisfies the requirement.
55
+ // Marketplace gating: the backend maps `git → github` for display
56
+ // (most-common-case heuristic); GitLab users hit the same workflow
57
+ // and gitSkill picks up GITLAB_TOKEN at runtime.
58
+ skills: [SKILLS.GIT],
22
59
  outputSchema: SetupOutputSchema,
23
- execute: async (state) => {
24
- console.log('\n🔧 Setting up environment...');
25
-
26
- const { workspace, repos, githubToken } = state;
27
- const gitlabToken = process.env.GITLAB_TOKEN || '';
28
- const gitlabUrl = process.env.GITLAB_URL || '';
29
-
30
- // DEBUG: Log token status
31
- console.log(`🔑 GitHub Token: ${githubToken ? 'Present' : 'MISSING'}`);
32
- console.log(`🔑 GitLab Token: ${gitlabToken ? 'Present' : 'MISSING'}`);
33
- if (gitlabUrl) console.log(`🔑 GitLab URL: ${gitlabUrl}`);
34
-
35
- // Log environment
36
- console.log('Container: ECS Fargate');
37
- console.log('Memory: 4GB');
38
- console.log('CPU: 2 vCPU');
39
- console.log('Tools: Node.js, Git, Cursor CLI, Zibby CLI');
40
- console.log(`Working directory: ${workspace}`);
41
-
42
- // Clone repositories
43
- console.log('\n📦 Cloning repositories...');
44
-
45
- const clonedRepos = [];
46
- for (const repo of repos) {
47
- console.log(`Cloning ${repo.name}...`);
48
-
49
- const repoDir = join(workspace, repo.name);
50
-
51
- // Use token for authentication based on provider
52
- let cloneUrl = repo.url;
53
- let cloneEnv = {};
54
- const isGitlab = repo.provider === 'gitlab' || (gitlabUrl && repo.url.includes(new URL(gitlabUrl).host));
55
- const isGithub = repo.provider === 'github' || repo.url.includes('github.com');
56
-
57
- if (isGithub && githubToken) {
58
- cloneUrl = repo.url.replace('https://github.com', `https://x-access-token:${githubToken}@github.com`);
59
- cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' };
60
- } else if (isGitlab && gitlabToken && gitlabUrl) {
61
- try {
62
- const gitlabHost = new URL(gitlabUrl).host;
63
- cloneUrl = repo.url.replace(`https://${gitlabHost}`, `https://oauth2:${gitlabToken}@${gitlabHost}`);
64
- } catch (e) {
65
- console.warn(`⚠️ Failed to parse GITLAB_URL: ${e.message}`);
66
- }
67
- cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' };
68
- }
69
-
70
- // Shallow clone with progress output (async, non-blocking)
71
- await execCommand(
72
- `git clone --progress --depth 1 --branch ${repo.branch} "${cloneUrl}" "${repoDir}"`,
73
- workspace,
74
- cloneEnv
75
- );
76
- console.log(`✓ Cloned ${repo.name} on branch ${repo.branch}`);
77
-
78
- clonedRepos.push({
79
- name: repo.name,
80
- path: repoDir,
81
- isPrimary: repo.isPrimary
82
- });
83
- }
84
-
85
- // Initialize git in workspace for diff tracking
86
- await execCommand('git init', workspace);
87
- await execCommand('git config user.email "zibby@agent.com"', workspace);
88
- await execCommand('git config user.name "Zibby Agent"', workspace);
89
- await execCommand('git add .', workspace);
90
- await execCommand('git commit --allow-empty -m "baseline"', workspace);
91
-
92
- console.log('✅ Environment ready');
93
-
94
- const baselineCommit = await execCommand('git rev-parse HEAD', workspace);
95
-
96
- return {
97
- success: true,
98
- clonedRepos,
99
- baselineCommit: baselineCommit.trim()
100
- };
101
- }
60
+ timeout: 5 * 60 * 1000, // 5min for cold clone of multiple repos + baseline
61
+ prompt: setupPrompt,
102
62
  };
103
-
104
- // Async version using spawn - streams output in real-time, doesn't block event loop
105
- async function execCommand(command, cwd, env = {}) {
106
- return new Promise((resolve, reject) => {
107
- const proc = spawn(command, {
108
- cwd,
109
- shell: true,
110
- env: Object.keys(env).length > 0 ? env : process.env
111
- });
112
-
113
- let stdout = '';
114
- let stderr = '';
115
-
116
- // Stream stdout as it comes (triggers middleware setInterval!)
117
- proc.stdout.on('data', (data) => {
118
- const output = data.toString();
119
- stdout += output;
120
- console.log(output.trimEnd());
121
- });
122
-
123
- // Stream stderr as it comes
124
- proc.stderr.on('data', (data) => {
125
- const output = data.toString();
126
- stderr += output;
127
- console.log(output.trimEnd());
128
- });
129
-
130
- proc.on('close', (code) => {
131
- if (code !== 0) {
132
- reject(new Error(`Command failed with exit code ${code}: ${command}`));
133
- } else {
134
- resolve(stdout || stderr || '');
135
- }
136
- });
137
-
138
- proc.on('error', (err) => {
139
- reject(new Error(`Command error: ${command} - ${err.message}`));
140
- });
141
- });
142
- }
@@ -0,0 +1,32 @@
1
+ import { join } from 'path';
2
+
3
+ /**
4
+ * Resolve the on-disk path for a repo.
5
+ *
6
+ * Source of truth: state.setup.clonedRepos[].path — populated by the
7
+ * LLM-driven setup node that calls git_checkout (via the `git` skill).
8
+ *
9
+ * Fallback: <workspace>/<name>. Kept so a workflow that never ran
10
+ * setup (e.g. zibby workflow run for unit testing a single node) still
11
+ * has a sensible path. Returns null if neither state.setup nor a
12
+ * workspace is available.
13
+ *
14
+ * Accepts either a raw state object OR a WorkflowState instance with
15
+ * .getAll(). Downstream nodes use both shapes across the template.
16
+ *
17
+ * @param {object | { getAll(): object }} stateOrAllState
18
+ * @param {string} repoName
19
+ * @returns {string | null}
20
+ */
21
+ export function getRepoPath(stateOrAllState, repoName) {
22
+ if (!stateOrAllState || !repoName) return null;
23
+ const s = typeof stateOrAllState.getAll === 'function'
24
+ ? stateOrAllState.getAll()
25
+ : stateOrAllState;
26
+ const cloned = Array.isArray(s.setup?.clonedRepos)
27
+ ? s.setup.clonedRepos.find((r) => r?.name === repoName)
28
+ : null;
29
+ if (cloned?.path) return cloned.path;
30
+ if (s.workspace) return join(s.workspace, repoName);
31
+ return null;
32
+ }
@@ -0,0 +1,71 @@
1
+ # Setup — clone repos + initialize baseline
2
+
3
+ You are setting up the workspace for code analysis. Two tasks:
4
+
5
+ ## 1. Clone each repo in `state.repos`
6
+
7
+ ```
8
+ state.repos = {{state.repos}}
9
+ state.workspace = {{state.workspace}}
10
+ ```
11
+
12
+ For each entry, call `git_checkout`:
13
+ - `url`: the repo's `url` field
14
+ - `branch`: the repo's `branch` field (omit if not set — defaults to repo's default branch)
15
+ - `shallow`: true (the default — faster, depth=1)
16
+
17
+ `git_checkout` auto-authenticates against GitHub or GitLab using the
18
+ project's connected integration — you don't need to know which provider
19
+ the repo is hosted on. If the clone fails for one repo, continue with
20
+ the others and surface the failure in the final output.
21
+
22
+ After every repo is cloned, the `path` field that `git_checkout` returns
23
+ is what downstream nodes will read off state. Keep that path verbatim.
24
+
25
+ ## 2. Initialize a baseline git repo at `state.workspace`
26
+
27
+ Downstream nodes (especially `generate_code`) diff your future changes
28
+ against this baseline. Use the Bash tool:
29
+
30
+ ```bash
31
+ cd {{state.workspace}}
32
+ git init
33
+ git config user.email "zibby@agent.com"
34
+ git config user.name "Zibby Agent"
35
+ git add .
36
+ git commit --allow-empty -m "baseline"
37
+ git rev-parse HEAD
38
+ ```
39
+
40
+ The output of `git rev-parse HEAD` is the `baselineCommit` you'll return.
41
+
42
+ ## 3. Return JSON matching the outputSchema
43
+
44
+ ```json
45
+ {
46
+ "success": true,
47
+ "clonedRepos": [
48
+ { "name": "<repo name>", "path": "<absolute path from git_checkout>", "isPrimary": true }
49
+ ],
50
+ "baselineCommit": "<HEAD sha from rev-parse>"
51
+ }
52
+ ```
53
+
54
+ If any repo fails to clone, set `success: false` and still include the
55
+ ones that succeeded with their paths.
56
+
57
+ ---
58
+
59
+ ### Customizing this prompt
60
+
61
+ This file ships as a template default. Edit it freely to change setup
62
+ behavior — e.g.:
63
+ - Skip the baseline commit if you don't need diff tracking
64
+ - Add a `git_explore` call after cloning to give later nodes a structural
65
+ overview in state
66
+ - Filter `state.repos` (only clone repos matching a pattern)
67
+ - Run post-clone tooling (`npm install`, `bundle install`, etc) via Bash
68
+
69
+ The agent will follow whatever instructions you put here. The
70
+ outputSchema in `nodes/setup-node.js` is the contract — keep that field
71
+ shape so downstream nodes don't break.
@@ -70,6 +70,22 @@ export const analysisContextSchema = z.object({
70
70
 
71
71
  nodeConfigs: z.record(z.string(), z.any()).optional()
72
72
  .describe('Per-node configuration overrides — set at deploy time, not trigger time'),
73
+
74
+ // ── Node outputs (mid-graph, runner-populated) ────────────────────
75
+ // Each LLM node writes its outputSchema-validated result at
76
+ // state.<nodeName>. These are declared optional so initialState
77
+ // validates before any node has run.
78
+
79
+ setup: z.object({
80
+ success: z.boolean(),
81
+ clonedRepos: z.array(z.object({
82
+ name: z.string(),
83
+ path: z.string(),
84
+ isPrimary: z.boolean().optional(),
85
+ })),
86
+ baselineCommit: z.string(),
87
+ }).optional()
88
+ .describe('Output of the setup node — clone paths + baseline commit (downstream nodes read clonedRepos[].path)'),
73
89
  });
74
90
 
75
91
  // Derived: what the engine actually validates initialState against at
@@ -18,6 +18,9 @@
18
18
  * sync; the win is each template is independently editable.
19
19
  */
20
20
 
21
+ import { readFileSync, existsSync } from 'fs';
22
+ import { join, dirname } from 'path';
23
+ import { fileURLToPath } from 'url';
21
24
  import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
22
25
  import { setupNode } from './nodes/setup-node.js';
23
26
  import { generateTestCasesNode } from './nodes/generate-test-cases-node.js';
@@ -26,6 +29,13 @@ import {
26
29
  generateTestCasesContextSchema,
27
30
  } from './state.js';
28
31
 
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ function loadPrompt(filename) {
34
+ const path = join(__dirname, 'prompts', filename);
35
+ return existsSync(path) ? readFileSync(path, 'utf-8') : null;
36
+ }
37
+ const setupPrompt = loadPrompt('setup.md');
38
+
29
39
  export class GenerateTestCasesAgent extends WorkflowAgent {
30
40
  buildGraph() {
31
41
  const graph = new WorkflowGraph();
@@ -33,7 +43,7 @@ export class GenerateTestCasesAgent extends WorkflowAgent {
33
43
  .setInputSchema(generateTestCasesInputSchema)
34
44
  .setContextSchema(generateTestCasesContextSchema);
35
45
 
36
- graph.addNode('setup', setupNode);
46
+ graph.addNode('setup', setupNode, { prompt: setupPrompt });
37
47
  graph.addNode('generate_test_cases', generateTestCasesNode);
38
48
 
39
49
  graph.setEntryPoint('setup');