@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.
- package/browser-test-automation/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/browser-test-automation/package.json +1 -0
- package/code-analysis/graph.js +5 -4
- package/code-analysis/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/code-analysis/nodes/analyze-ticket-node.js +9 -2
- package/code-analysis/nodes/generate-code-node.js +27 -11
- package/code-analysis/nodes/setup-node.js +50 -130
- package/code-analysis/nodes/utils/get-repo-path.js +32 -0
- package/code-analysis/prompts/setup.md +71 -0
- package/code-analysis/state.js +16 -0
- package/generate-test-cases/graph.mjs +11 -1
- package/generate-test-cases/nodes/setup-node.js +32 -130
- package/generate-test-cases/prompts/setup.md +50 -0
- package/generate-test-cases/state.js +12 -0
- package/index.js +136 -0
- package/notify-lark/README.md +88 -0
- package/notify-lark/graph.mjs +43 -0
- package/notify-lark/icon.png +0 -0
- package/notify-lark/nodes/notify-lark-node.js +290 -0
- package/notify-lark/package.json +18 -0
- package/notify-lark/state.js +75 -0
- package/notify-slack/README.md +94 -0
- package/notify-slack/graph.mjs +51 -0
- package/notify-slack/icon.png +0 -0
- package/notify-slack/nodes/notify-slack-node.js +238 -0
- package/notify-slack/package.json +18 -0
- package/notify-slack/state.js +93 -0
- package/package.json +12 -3
- package/sentry-triage/graph.mjs +81 -0
- package/sentry-triage/icon.png +0 -0
- package/sentry-triage/nodes/classify-node.js +38 -0
- package/sentry-triage/nodes/dispatch-alerts-node.js +191 -0
- package/sentry-triage/nodes/fetch-issues-node.js +52 -0
- package/sentry-triage/nodes/filter-noise-node.js +112 -0
- package/sentry-triage/package.json +18 -0
- package/sentry-triage/prompts/classify.md +76 -0
- package/sentry-triage/prompts/fetch-issues.md +66 -0
- 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}]]}
|
package/code-analysis/graph.js
CHANGED
|
@@ -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 '
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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,
|
|
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(
|
|
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 -
|
|
3
|
-
*
|
|
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 {
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
import {
|
|
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
|
-
|
|
24
|
-
|
|
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.
|
package/code-analysis/state.js
CHANGED
|
@@ -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');
|