@zibby/workflow-templates 0.2.1 → 0.3.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.
@@ -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) => {
@@ -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
 
@@ -11,6 +11,7 @@ import Handlebars from 'handlebars';
11
11
  import { invokeAgent } from '@zibby/core';
12
12
  import { generatePRMeta } from './services/prMetaService.js';
13
13
  import { adfToText } from '@zibby/core/utils/adf-converter.js';
14
+ import { getRepoPath } from './utils/get-repo-path.js';
14
15
  import { z } from 'zod';
15
16
 
16
17
  const CodeImplementationOutputSchema = z.object({
@@ -64,14 +65,16 @@ export function createCodeGenerationNode(options = {}) {
64
65
  const _nodeConfig = nodeConfigs[nodeName] || {};
65
66
  const analysis = state.analyze_ticket?.analysis;
66
67
 
67
- // Build implementation prompt from template
68
+ // Build implementation prompt from template. Pass `state` so the
69
+ // prompt builder can resolve cloned repo paths from state.setup
70
+ // (populated by the LLM-driven setup node).
68
71
  const implementPrompt = buildImplementationPrompt(
69
72
  promptsDir,
70
73
  ticketContext,
71
74
  analysis,
72
75
  commitAndPush,
73
76
  repos,
74
- workspace
77
+ state
75
78
  );
76
79
 
77
80
  console.log(`🚀 Running AI Agent to ${mode} changes with model: ${aiModel}...`);
@@ -96,7 +99,7 @@ export function createCodeGenerationNode(options = {}) {
96
99
  commitMessage = `${ticketContext.ticketKey}: ${ticketContext.summary}\n\n${commitDesc}\n\nImplemented by Zibby Agent`;
97
100
 
98
101
  for (const repo of reposToCommit) {
99
- const repoPath = join(workspace, repo.name);
102
+ const repoPath = getRepoPath(state, repo.name);
100
103
 
101
104
  // Check if this repo has any changes
102
105
  const status = await execCommand('git', ['status', '--porcelain'], repoPath);
@@ -153,9 +156,9 @@ export function createCodeGenerationNode(options = {}) {
153
156
  : [{ name: '.' }];
154
157
 
155
158
  for (const repo of reposToCheck) {
156
- const currentRepoPath = repo.name === '.'
157
- ? workspace
158
- : join(workspace, repo.name);
159
+ const currentRepoPath = repo.name === '.'
160
+ ? workspace
161
+ : getRepoPath(state, repo.name);
159
162
 
160
163
  try {
161
164
  let repoDiff, repoDiffStat, repoChangedFiles;
@@ -314,7 +317,8 @@ export const implementCodeNode = createCodeGenerationNode({
314
317
  nodeName: 'implement_code'
315
318
  });
316
319
 
317
- function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, workspace) {
320
+ function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, state) {
321
+ const workspace = state?.workspace;
318
322
  const templatePath = join(promptsDir, 'generate-code.md');
319
323
  if (!existsSync(templatePath)) {
320
324
  throw new Error(`Template not found: ${templatePath}`);
@@ -342,9 +346,11 @@ function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommit
342
346
  }
343
347
  }
344
348
 
345
- // Build repo metadata for the template
349
+ // Build repo metadata for the template. Use getRepoPath() so we
350
+ // detect language from wherever the setup node actually checked the
351
+ // repo out (state.setup.clonedRepos[].path).
346
352
  const repositories = (repos && repos.length > 0) ? repos.map(r => {
347
- const info = detectRepoLanguage(workspace ? join(workspace, r.name) : null);
353
+ const info = detectRepoLanguage(getRepoPath(state, r.name));
348
354
  return { name: r.name, ...info };
349
355
  }) : null;
350
356
 
@@ -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');
@@ -1,142 +1,44 @@
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
+ * Mirrors code-analysis/setup-node.js. The agent receives the `git`
5
+ * skill (git_checkout, git_list_repos, git_explore) plus Bash, and is
6
+ * prompted to clone each repo from state.repos and initialize a
7
+ * baseline commit at state.workspace for diff tracking. The output
8
+ * lands at state.setup.{clonedRepos[], baselineCommit} where downstream
9
+ * nodes read it.
10
+ *
11
+ * Prompt is loaded by graph.mjs from prompts/setup.md so users can
12
+ * customize setup behavior without editing this file.
4
13
  */
5
14
 
6
- import { spawn } from 'child_process';
7
- import { join } from 'path';
8
- import { z } from 'zod';
15
+ import { readFileSync, existsSync } from 'fs';
16
+ import { dirname, join } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { z, SKILLS } from '@zibby/core';
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const promptPath = join(__dirname, '..', 'prompts', 'setup.md');
22
+ const setupPrompt = existsSync(promptPath)
23
+ ? readFileSync(promptPath, 'utf-8')
24
+ : '';
9
25
 
10
26
  const SetupOutputSchema = z.object({
11
- success: z.boolean(),
27
+ success: z.boolean().describe('true if all repos cloned + baseline initialized'),
12
28
  clonedRepos: z.array(z.object({
13
- name: z.string(),
14
- path: z.string(),
15
- isPrimary: z.boolean().optional()
16
- })),
17
- baselineCommit: z.string()
29
+ name: z.string().describe('Repo name (matches state.repos[].name)'),
30
+ path: z.string().describe('Absolute local path where the repo was cloned'),
31
+ isPrimary: z.boolean().optional().describe('True for the project\'s primary repo'),
32
+ })).describe('Every repo from state.repos must appear here once'),
33
+ baselineCommit: z.string().describe('git rev-parse HEAD at state.workspace after baseline commit'),
18
34
  });
19
35
 
20
36
  export const setupNode = {
21
37
  name: 'setup',
38
+ // SKILLS.GIT gives the LLM clone tools that auto-auth against GitHub
39
+ // OR GitLab — either connected integration satisfies the workflow.
40
+ skills: [SKILLS.GIT],
22
41
  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
- }
42
+ timeout: 5 * 60 * 1000,
43
+ prompt: setupPrompt,
102
44
  };
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,50 @@
1
+ # Setup — clone repos + initialize baseline
2
+
3
+ You are setting up the workspace for test-case generation. 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)
15
+ - `shallow`: true (default)
16
+
17
+ `git_checkout` auto-authenticates against GitHub or GitLab using the
18
+ project's connected integration. If one repo fails, continue with the
19
+ others and surface the failure in the final output.
20
+
21
+ ## 2. Initialize a baseline git repo at `state.workspace`
22
+
23
+ ```bash
24
+ cd {{state.workspace}}
25
+ git init
26
+ git config user.email "zibby@agent.com"
27
+ git config user.name "Zibby Agent"
28
+ git add .
29
+ git commit --allow-empty -m "baseline"
30
+ git rev-parse HEAD
31
+ ```
32
+
33
+ The `git rev-parse HEAD` output is the `baselineCommit` you'll return.
34
+
35
+ ## 3. Return JSON matching the outputSchema
36
+
37
+ ```json
38
+ {
39
+ "success": true,
40
+ "clonedRepos": [
41
+ { "name": "<repo name>", "path": "<absolute path from git_checkout>", "isPrimary": true }
42
+ ],
43
+ "baselineCommit": "<HEAD sha>"
44
+ }
45
+ ```
46
+
47
+ ---
48
+
49
+ Edit this prompt to customize setup behavior — e.g. skip baseline,
50
+ filter repos, or run post-clone tooling (`npm install`, etc).
@@ -59,6 +59,18 @@ export const generateTestCasesContextSchema = z.object({
59
59
 
60
60
  nodeConfigs: z.record(z.string(), z.any()).optional()
61
61
  .describe('Per-node configuration overrides — set at deploy time, not trigger time'),
62
+
63
+ // ── Node outputs (mid-graph, runner-populated) ────────────────────
64
+ setup: z.object({
65
+ success: z.boolean(),
66
+ clonedRepos: z.array(z.object({
67
+ name: z.string(),
68
+ path: z.string(),
69
+ isPrimary: z.boolean().optional(),
70
+ })),
71
+ baselineCommit: z.string(),
72
+ }).optional()
73
+ .describe('Output of the setup node — clone paths + baseline commit'),
62
74
  });
63
75
 
64
76
  // Derived: full runtime state. Exported for tests + tooling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/workflow-templates",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Built-in workflow templates for Zibby — browser-test-automation, code-analysis, generate-test-cases. Carved out of @zibby/core@0.4.6 so the engine ships without scaffolding payload.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@anthropic-ai/sdk": "^0.88.0",
60
- "@zibby/agent-workflow": "^0.4.0",
60
+ "@zibby/agent-workflow": "^0.4.1",
61
61
  "axios": "^1.15.0",
62
62
  "handlebars": "^4.7.9",
63
63
  "zod": "^3.23.0 || ^4.0.0"