@itz4blitz/agentful 1.2.0 → 1.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.
- package/README.md +28 -1
- package/bin/cli.js +11 -1055
- package/bin/hooks/block-file-creation.js +271 -0
- package/bin/hooks/product-spec-watcher.js +151 -0
- package/lib/index.js +0 -11
- package/lib/init.js +2 -21
- package/lib/parallel-execution.js +235 -0
- package/lib/presets.js +26 -4
- package/package.json +4 -7
- package/template/.claude/agents/architect.md +2 -2
- package/template/.claude/agents/backend.md +17 -30
- package/template/.claude/agents/frontend.md +17 -39
- package/template/.claude/agents/orchestrator.md +63 -4
- package/template/.claude/agents/product-analyzer.md +1 -1
- package/template/.claude/agents/tester.md +16 -29
- package/template/.claude/commands/agentful-generate.md +221 -14
- package/template/.claude/commands/agentful-init.md +621 -0
- package/template/.claude/commands/agentful-product.md +1 -1
- package/template/.claude/commands/agentful-start.md +99 -1
- package/template/.claude/product/EXAMPLES.md +2 -2
- package/template/.claude/product/index.md +1 -1
- package/template/.claude/settings.json +22 -0
- package/template/.claude/skills/research/SKILL.md +432 -0
- package/template/CLAUDE.md +5 -6
- package/template/bin/hooks/architect-drift-detector.js +242 -0
- package/template/bin/hooks/product-spec-watcher.js +151 -0
- package/version.json +1 -1
- package/bin/hooks/post-agent.js +0 -101
- package/bin/hooks/post-feature.js +0 -227
- package/bin/hooks/pre-agent.js +0 -118
- package/bin/hooks/pre-feature.js +0 -138
- package/lib/VALIDATION_README.md +0 -455
- package/lib/ci/claude-action-integration.js +0 -641
- package/lib/ci/index.js +0 -10
- package/lib/core/analyzer.js +0 -497
- package/lib/core/cli.js +0 -141
- package/lib/core/detectors/conventions.js +0 -342
- package/lib/core/detectors/framework.js +0 -276
- package/lib/core/detectors/index.js +0 -15
- package/lib/core/detectors/language.js +0 -199
- package/lib/core/detectors/patterns.js +0 -356
- package/lib/core/generator.js +0 -626
- package/lib/core/index.js +0 -9
- package/lib/core/output-parser.js +0 -458
- package/lib/core/storage.js +0 -515
- package/lib/core/templates.js +0 -556
- package/lib/pipeline/cli.js +0 -423
- package/lib/pipeline/engine.js +0 -928
- package/lib/pipeline/executor.js +0 -440
- package/lib/pipeline/index.js +0 -33
- package/lib/pipeline/integrations.js +0 -559
- package/lib/pipeline/schemas.js +0 -288
- package/lib/remote/client.js +0 -361
- package/lib/server/auth.js +0 -270
- package/lib/server/client-example.js +0 -190
- package/lib/server/executor.js +0 -477
- package/lib/server/index.js +0 -494
- package/lib/update-helpers.js +0 -505
- package/lib/validation.js +0 -460
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code Action Integration
|
|
3
|
-
*
|
|
4
|
-
* Integrates agentful with anthropics/claude-code-action for CI/CD.
|
|
5
|
-
* Provides tools to load agent definitions, build prompts, and generate workflows.
|
|
6
|
-
*
|
|
7
|
-
* @module ci/claude-action-integration
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import fs from 'fs/promises';
|
|
11
|
-
import path from 'path';
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
13
|
-
import yaml from 'js-yaml';
|
|
14
|
-
|
|
15
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
-
const __dirname = path.dirname(__filename);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Error codes for CI operations
|
|
20
|
-
*/
|
|
21
|
-
export const CI_ERROR_CODES = {
|
|
22
|
-
AGENT_NOT_FOUND: 'AGENT_NOT_FOUND',
|
|
23
|
-
INVALID_AGENT_FILE: 'INVALID_AGENT_FILE',
|
|
24
|
-
MISSING_CONTEXT: 'MISSING_CONTEXT',
|
|
25
|
-
WORKFLOW_GENERATION_FAILED: 'WORKFLOW_GENERATION_FAILED',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* CI Error class for structured error handling
|
|
30
|
-
*/
|
|
31
|
-
export class CIError extends Error {
|
|
32
|
-
constructor(code, message, details = {}) {
|
|
33
|
-
super(message);
|
|
34
|
-
this.name = 'CIError';
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.details = details;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Validate agent name to prevent path traversal
|
|
42
|
-
* @param {string} agentName - Agent name to validate
|
|
43
|
-
* @returns {boolean} True if valid
|
|
44
|
-
*/
|
|
45
|
-
function isValidAgentName(agentName) {
|
|
46
|
-
// Only allow alphanumeric, hyphens, and underscores
|
|
47
|
-
return /^[a-zA-Z0-9_-]+$/.test(agentName);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse agent markdown file with frontmatter
|
|
52
|
-
* @param {string} content - Raw markdown content
|
|
53
|
-
* @returns {Object} Parsed agent definition { metadata, instructions }
|
|
54
|
-
*/
|
|
55
|
-
function parseAgentMarkdown(content) {
|
|
56
|
-
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
57
|
-
const match = content.match(frontmatterRegex);
|
|
58
|
-
|
|
59
|
-
if (!match) {
|
|
60
|
-
throw new CIError(
|
|
61
|
-
CI_ERROR_CODES.INVALID_AGENT_FILE,
|
|
62
|
-
'Agent file must have YAML frontmatter',
|
|
63
|
-
{ content: content.substring(0, 200) }
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const [, frontmatter, instructions] = match;
|
|
68
|
-
|
|
69
|
-
let metadata;
|
|
70
|
-
try {
|
|
71
|
-
metadata = yaml.load(frontmatter);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
throw new CIError(
|
|
74
|
-
CI_ERROR_CODES.INVALID_AGENT_FILE,
|
|
75
|
-
'Invalid YAML frontmatter in agent file',
|
|
76
|
-
{ error: error.message }
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
metadata,
|
|
82
|
-
instructions: instructions.trim(),
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Load agent definition from .claude/agents/{name}.md
|
|
88
|
-
* @param {string} agentName - Name of the agent (e.g., 'backend', 'frontend')
|
|
89
|
-
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
90
|
-
* @returns {Promise<Object>} Agent definition with metadata and instructions
|
|
91
|
-
*/
|
|
92
|
-
export async function loadAgentDefinition(agentName, projectRoot = process.cwd()) {
|
|
93
|
-
// Validate agent name to prevent path traversal
|
|
94
|
-
if (!isValidAgentName(agentName)) {
|
|
95
|
-
throw new CIError(
|
|
96
|
-
CI_ERROR_CODES.AGENT_NOT_FOUND,
|
|
97
|
-
`Invalid agent name: "${agentName}". Agent names must contain only alphanumeric characters, hyphens, and underscores.`,
|
|
98
|
-
{ agentName }
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const agentPath = path.join(projectRoot, '.claude', 'agents', `${agentName}.md`);
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const content = await fs.readFile(agentPath, 'utf-8');
|
|
106
|
-
const agent = parseAgentMarkdown(content);
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
name: agentName,
|
|
110
|
-
path: agentPath,
|
|
111
|
-
...agent,
|
|
112
|
-
};
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (error.code === 'ENOENT') {
|
|
115
|
-
throw new CIError(
|
|
116
|
-
CI_ERROR_CODES.AGENT_NOT_FOUND,
|
|
117
|
-
`Agent definition not found: ${agentName}`,
|
|
118
|
-
{ path: agentPath, availableAgents: await listAvailableAgents(projectRoot) }
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (error instanceof CIError) {
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw new CIError(
|
|
127
|
-
CI_ERROR_CODES.INVALID_AGENT_FILE,
|
|
128
|
-
`Failed to load agent definition: ${error.message}`,
|
|
129
|
-
{ agentName, path: agentPath, error: error.message }
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* List all available agents in the project
|
|
136
|
-
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
137
|
-
* @returns {Promise<string[]>} Array of agent names
|
|
138
|
-
*/
|
|
139
|
-
export async function listAvailableAgents(projectRoot = process.cwd()) {
|
|
140
|
-
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const files = await fs.readdir(agentsDir);
|
|
144
|
-
return files
|
|
145
|
-
.filter(f => f.endsWith('.md'))
|
|
146
|
-
.map(f => f.replace('.md', ''));
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Extract GitHub context from environment variables
|
|
154
|
-
* @returns {Promise<Object>} GitHub context metadata
|
|
155
|
-
*/
|
|
156
|
-
export async function extractGitHubContext() {
|
|
157
|
-
const context = {
|
|
158
|
-
platform: 'github',
|
|
159
|
-
event: process.env.GITHUB_EVENT_NAME,
|
|
160
|
-
repository: process.env.GITHUB_REPOSITORY,
|
|
161
|
-
ref: process.env.GITHUB_REF,
|
|
162
|
-
sha: process.env.GITHUB_SHA,
|
|
163
|
-
actor: process.env.GITHUB_ACTOR,
|
|
164
|
-
workflow: process.env.GITHUB_WORKFLOW,
|
|
165
|
-
runId: process.env.GITHUB_RUN_ID,
|
|
166
|
-
runNumber: process.env.GITHUB_RUN_NUMBER,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
// Parse PR-specific context
|
|
170
|
-
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
171
|
-
try {
|
|
172
|
-
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
173
|
-
if (eventPath) {
|
|
174
|
-
const eventData = await fs.readFile(eventPath, 'utf-8');
|
|
175
|
-
const event = JSON.parse(eventData);
|
|
176
|
-
context.pullRequest = {
|
|
177
|
-
number: event.pull_request?.number,
|
|
178
|
-
title: event.pull_request?.title,
|
|
179
|
-
author: event.pull_request?.user?.login,
|
|
180
|
-
base: event.pull_request?.base?.ref,
|
|
181
|
-
head: event.pull_request?.head?.ref,
|
|
182
|
-
changedFiles: event.pull_request?.changed_files,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
} catch (error) {
|
|
186
|
-
// Event data not available, continue without it
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return context;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Extract GitLab context from environment variables
|
|
195
|
-
* @returns {Object} GitLab context metadata
|
|
196
|
-
*/
|
|
197
|
-
export function extractGitLabContext() {
|
|
198
|
-
return {
|
|
199
|
-
platform: 'gitlab',
|
|
200
|
-
projectId: process.env.CI_PROJECT_ID,
|
|
201
|
-
projectName: process.env.CI_PROJECT_NAME,
|
|
202
|
-
repository: process.env.CI_PROJECT_PATH,
|
|
203
|
-
ref: process.env.CI_COMMIT_REF_NAME,
|
|
204
|
-
sha: process.env.CI_COMMIT_SHA,
|
|
205
|
-
pipeline: process.env.CI_PIPELINE_ID,
|
|
206
|
-
job: process.env.CI_JOB_NAME,
|
|
207
|
-
mergeRequest: process.env.CI_MERGE_REQUEST_IID ? {
|
|
208
|
-
iid: process.env.CI_MERGE_REQUEST_IID,
|
|
209
|
-
title: process.env.CI_MERGE_REQUEST_TITLE,
|
|
210
|
-
sourceBranch: process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
211
|
-
targetBranch: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
212
|
-
} : null,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Extract Jenkins context from environment variables
|
|
218
|
-
* @returns {Object} Jenkins context metadata
|
|
219
|
-
*/
|
|
220
|
-
export function extractJenkinsContext() {
|
|
221
|
-
return {
|
|
222
|
-
platform: 'jenkins',
|
|
223
|
-
buildNumber: process.env.BUILD_NUMBER,
|
|
224
|
-
buildId: process.env.BUILD_ID,
|
|
225
|
-
jobName: process.env.JOB_NAME,
|
|
226
|
-
buildUrl: process.env.BUILD_URL,
|
|
227
|
-
gitBranch: process.env.GIT_BRANCH,
|
|
228
|
-
gitCommit: process.env.GIT_COMMIT,
|
|
229
|
-
changeId: process.env.CHANGE_ID,
|
|
230
|
-
changeBranch: process.env.CHANGE_BRANCH,
|
|
231
|
-
changeTarget: process.env.CHANGE_TARGET,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function extractBitbucketContext() {
|
|
236
|
-
return {
|
|
237
|
-
platform: 'bitbucket',
|
|
238
|
-
buildNumber: process.env.BITBUCKET_BUILD_NUMBER,
|
|
239
|
-
pipelineUuid: process.env.BITBUCKET_PIPELINE_UUID,
|
|
240
|
-
repository: process.env.BITBUCKET_REPO_FULL_NAME,
|
|
241
|
-
workspace: process.env.BITBUCKET_WORKSPACE,
|
|
242
|
-
gitBranch: process.env.BITBUCKET_BRANCH,
|
|
243
|
-
gitCommit: process.env.BITBUCKET_COMMIT,
|
|
244
|
-
gitTag: process.env.BITBUCKET_TAG,
|
|
245
|
-
pullRequest: process.env.BITBUCKET_PR_ID ? {
|
|
246
|
-
id: process.env.BITBUCKET_PR_ID,
|
|
247
|
-
destinationBranch: process.env.BITBUCKET_PR_DESTINATION_BRANCH,
|
|
248
|
-
} : null,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Auto-detect CI platform and extract context
|
|
254
|
-
* @returns {Promise<Object>} CI context metadata
|
|
255
|
-
*/
|
|
256
|
-
export async function extractCIContext() {
|
|
257
|
-
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
258
|
-
return await extractGitHubContext();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (process.env.GITLAB_CI === 'true') {
|
|
262
|
-
return extractGitLabContext();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (process.env.JENKINS_HOME) {
|
|
266
|
-
return extractJenkinsContext();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (process.env.BITBUCKET_BUILD_NUMBER) {
|
|
270
|
-
return extractBitbucketContext();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
platform: 'unknown',
|
|
275
|
-
isCI: false,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Inject CI context into prompt with proper formatting
|
|
281
|
-
* @param {string} prompt - Base prompt
|
|
282
|
-
* @param {Object} ciMetadata - CI context metadata
|
|
283
|
-
* @returns {string} Prompt with injected CI context
|
|
284
|
-
*/
|
|
285
|
-
export function injectCIContext(prompt, ciMetadata) {
|
|
286
|
-
const contextSection = `
|
|
287
|
-
## CI Context
|
|
288
|
-
|
|
289
|
-
**Platform:** ${ciMetadata.platform}
|
|
290
|
-
**Repository:** ${ciMetadata.repository || 'N/A'}
|
|
291
|
-
**Branch:** ${ciMetadata.ref || ciMetadata.gitBranch || 'N/A'}
|
|
292
|
-
**Commit:** ${ciMetadata.sha || ciMetadata.gitCommit || 'N/A'}
|
|
293
|
-
**Actor:** ${ciMetadata.actor || 'N/A'}
|
|
294
|
-
|
|
295
|
-
${ciMetadata.pullRequest ? `
|
|
296
|
-
### Pull Request
|
|
297
|
-
- **Number:** #${ciMetadata.pullRequest.number}
|
|
298
|
-
- **Title:** ${ciMetadata.pullRequest.title}
|
|
299
|
-
- **Author:** ${ciMetadata.pullRequest.author}
|
|
300
|
-
- **Base:** ${ciMetadata.pullRequest.base}
|
|
301
|
-
- **Head:** ${ciMetadata.pullRequest.head}
|
|
302
|
-
- **Changed Files:** ${ciMetadata.pullRequest.changedFiles || 'N/A'}
|
|
303
|
-
` : ''}
|
|
304
|
-
|
|
305
|
-
${ciMetadata.mergeRequest ? `
|
|
306
|
-
### Merge Request
|
|
307
|
-
- **IID:** !${ciMetadata.mergeRequest.iid}
|
|
308
|
-
- **Title:** ${ciMetadata.mergeRequest.title}
|
|
309
|
-
- **Source:** ${ciMetadata.mergeRequest.sourceBranch}
|
|
310
|
-
- **Target:** ${ciMetadata.mergeRequest.targetBranch}
|
|
311
|
-
` : ''}
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
`;
|
|
315
|
-
|
|
316
|
-
return contextSection + '\n' + prompt;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Build a prompt for claude-code-action
|
|
321
|
-
* @param {string} agentName - Name of the agent
|
|
322
|
-
* @param {string} task - Specific task description
|
|
323
|
-
* @param {Object} [options] - Optional configuration
|
|
324
|
-
* @param {string} [options.projectRoot] - Project root directory
|
|
325
|
-
* @param {Object} [options.context] - Additional context to inject
|
|
326
|
-
* @param {boolean} [options.includeCIContext=true] - Include CI metadata
|
|
327
|
-
* @returns {Promise<string>} Formatted prompt for claude-code-action
|
|
328
|
-
*/
|
|
329
|
-
export async function buildCIPrompt(agentName, task, options = {}) {
|
|
330
|
-
const {
|
|
331
|
-
projectRoot = process.cwd(),
|
|
332
|
-
context = {},
|
|
333
|
-
includeCIContext = true,
|
|
334
|
-
} = options;
|
|
335
|
-
|
|
336
|
-
// Load agent definition
|
|
337
|
-
const agent = await loadAgentDefinition(agentName, projectRoot);
|
|
338
|
-
|
|
339
|
-
// Build base prompt
|
|
340
|
-
let prompt = `# Task for ${agent.metadata.name} Agent
|
|
341
|
-
|
|
342
|
-
${task}
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
# Agent Instructions
|
|
347
|
-
|
|
348
|
-
${agent.instructions}
|
|
349
|
-
`;
|
|
350
|
-
|
|
351
|
-
// Add custom context if provided
|
|
352
|
-
if (Object.keys(context).length > 0) {
|
|
353
|
-
prompt += `\n\n## Additional Context\n\n`;
|
|
354
|
-
for (const [key, value] of Object.entries(context)) {
|
|
355
|
-
prompt += `**${key}:** ${value}\n`;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Inject CI context if enabled
|
|
360
|
-
if (includeCIContext) {
|
|
361
|
-
const ciContext = await extractCIContext();
|
|
362
|
-
if (ciContext.platform !== 'unknown') {
|
|
363
|
-
prompt = injectCIContext(prompt, ciContext);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return prompt;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Generate GitHub Actions workflow file
|
|
372
|
-
* @param {Object} config - Workflow configuration
|
|
373
|
-
* @param {string[]} config.agents - Agent names to include
|
|
374
|
-
* @param {string[]} config.triggers - Event triggers (e.g., ['pull_request', 'push'])
|
|
375
|
-
* @param {Object} [config.options] - Additional workflow options
|
|
376
|
-
* @returns {Promise<string>} YAML workflow content
|
|
377
|
-
*/
|
|
378
|
-
export async function generateGitHubWorkflow(config) {
|
|
379
|
-
const {
|
|
380
|
-
agents = ['backend', 'frontend', 'reviewer'],
|
|
381
|
-
triggers = ['pull_request'],
|
|
382
|
-
options = {},
|
|
383
|
-
} = config;
|
|
384
|
-
|
|
385
|
-
const {
|
|
386
|
-
nodeVersion = '22.x',
|
|
387
|
-
runsOn = 'ubuntu-latest',
|
|
388
|
-
branches = ['main', 'develop'],
|
|
389
|
-
} = options;
|
|
390
|
-
|
|
391
|
-
const workflow = {
|
|
392
|
-
name: 'Agentful CI',
|
|
393
|
-
on: {},
|
|
394
|
-
jobs: {},
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// Configure triggers
|
|
398
|
-
for (const trigger of triggers) {
|
|
399
|
-
if (trigger === 'pull_request' || trigger === 'push') {
|
|
400
|
-
workflow.on[trigger] = {
|
|
401
|
-
branches,
|
|
402
|
-
};
|
|
403
|
-
} else {
|
|
404
|
-
workflow.on[trigger] = {};
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Generate job for each agent
|
|
409
|
-
for (const agentName of agents) {
|
|
410
|
-
const jobId = agentName.replace(/[^a-z0-9_-]/gi, '-');
|
|
411
|
-
|
|
412
|
-
workflow.jobs[jobId] = {
|
|
413
|
-
name: `${agentName} Agent`,
|
|
414
|
-
'runs-on': runsOn,
|
|
415
|
-
steps: [
|
|
416
|
-
{
|
|
417
|
-
name: 'Checkout code',
|
|
418
|
-
uses: 'actions/checkout@v4',
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
name: 'Setup Node.js',
|
|
422
|
-
uses: 'actions/setup-node@v4',
|
|
423
|
-
with: {
|
|
424
|
-
'node-version': nodeVersion,
|
|
425
|
-
cache: 'npm',
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
name: 'Install dependencies',
|
|
430
|
-
run: 'npm ci',
|
|
431
|
-
},
|
|
432
|
-
{
|
|
433
|
-
name: `Run ${agentName} agent`,
|
|
434
|
-
uses: 'anthropics/claude-code-action@v1',
|
|
435
|
-
with: {
|
|
436
|
-
prompt: `$\{{ steps.prepare-prompt.outputs.prompt }}`,
|
|
437
|
-
'api-key': '${{ secrets.ANTHROPIC_API_KEY }}',
|
|
438
|
-
model: 'claude-sonnet-4',
|
|
439
|
-
},
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
// Add step to prepare prompt
|
|
445
|
-
workflow.jobs[jobId].steps.splice(3, 0, {
|
|
446
|
-
name: 'Prepare agent prompt',
|
|
447
|
-
id: 'prepare-prompt',
|
|
448
|
-
run: `echo "prompt=$(npx agentful ci ${agentName} \\"Analyze and review changes\\")" >> $GITHUB_OUTPUT`,
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return yaml.dump(workflow, {
|
|
453
|
-
lineWidth: 120,
|
|
454
|
-
noRefs: true,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Generate GitLab CI configuration
|
|
460
|
-
* @param {Object} config - CI configuration
|
|
461
|
-
* @returns {Promise<string>} YAML GitLab CI content
|
|
462
|
-
*/
|
|
463
|
-
export async function generateGitLabCI(config) {
|
|
464
|
-
const {
|
|
465
|
-
agents = ['backend', 'frontend', 'reviewer'],
|
|
466
|
-
options = {},
|
|
467
|
-
} = config;
|
|
468
|
-
|
|
469
|
-
const {
|
|
470
|
-
image = 'node:22',
|
|
471
|
-
stages = ['test', 'review'],
|
|
472
|
-
} = options;
|
|
473
|
-
|
|
474
|
-
const ci = {
|
|
475
|
-
image,
|
|
476
|
-
stages,
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
// Generate job for each agent
|
|
480
|
-
for (const agentName of agents) {
|
|
481
|
-
const jobName = `${agentName}_agent`;
|
|
482
|
-
|
|
483
|
-
ci[jobName] = {
|
|
484
|
-
stage: 'review',
|
|
485
|
-
script: [
|
|
486
|
-
'npm ci',
|
|
487
|
-
`PROMPT=$(npx agentful ci ${agentName} "Analyze and review changes")`,
|
|
488
|
-
'echo "$PROMPT" | claude-code-action',
|
|
489
|
-
],
|
|
490
|
-
only: ['merge_requests'],
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return yaml.dump(ci, {
|
|
495
|
-
lineWidth: 120,
|
|
496
|
-
noRefs: true,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Generate Jenkinsfile
|
|
502
|
-
* @param {Object} config - Pipeline configuration
|
|
503
|
-
* @returns {Promise<string>} Jenkinsfile content
|
|
504
|
-
*/
|
|
505
|
-
export async function generateJenkinsfile(config) {
|
|
506
|
-
const {
|
|
507
|
-
agents = ['backend', 'frontend', 'reviewer'],
|
|
508
|
-
options = {},
|
|
509
|
-
} = config;
|
|
510
|
-
|
|
511
|
-
const {
|
|
512
|
-
nodeVersion = '22',
|
|
513
|
-
} = options;
|
|
514
|
-
|
|
515
|
-
let jenkinsfile = `
|
|
516
|
-
pipeline {
|
|
517
|
-
agent any
|
|
518
|
-
|
|
519
|
-
tools {
|
|
520
|
-
nodejs '${nodeVersion}'
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
stages {
|
|
524
|
-
stage('Setup') {
|
|
525
|
-
steps {
|
|
526
|
-
sh 'npm ci'
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
`;
|
|
530
|
-
|
|
531
|
-
// Generate stage for each agent
|
|
532
|
-
for (const agentName of agents) {
|
|
533
|
-
jenkinsfile += `
|
|
534
|
-
stage('${agentName} Agent') {
|
|
535
|
-
steps {
|
|
536
|
-
script {
|
|
537
|
-
def prompt = sh(
|
|
538
|
-
script: "npx agentful ci ${agentName} 'Analyze and review changes'",
|
|
539
|
-
returnStdout: true
|
|
540
|
-
).trim()
|
|
541
|
-
|
|
542
|
-
// Run claude-code-action with prompt
|
|
543
|
-
sh "echo '\${prompt}' | claude-code-action"
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
`;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
jenkinsfile += `
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
post {
|
|
554
|
-
always {
|
|
555
|
-
cleanWs()
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
`;
|
|
560
|
-
|
|
561
|
-
return jenkinsfile.trim();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Generate workflow for specified platform
|
|
566
|
-
* @param {Object} config - Workflow configuration
|
|
567
|
-
* @param {string} config.platform - CI platform ('github', 'gitlab', 'jenkins')
|
|
568
|
-
* @param {string[]} config.agents - Agent names
|
|
569
|
-
* @param {string[]} [config.triggers] - Event triggers (GitHub Actions only)
|
|
570
|
-
* @param {Object} [config.options] - Additional options
|
|
571
|
-
* @returns {Promise<string>} Workflow configuration content
|
|
572
|
-
*/
|
|
573
|
-
export async function generateWorkflow(config) {
|
|
574
|
-
const { platform = 'github' } = config;
|
|
575
|
-
|
|
576
|
-
switch (platform) {
|
|
577
|
-
case 'github':
|
|
578
|
-
return generateGitHubWorkflow(config);
|
|
579
|
-
case 'gitlab':
|
|
580
|
-
return generateGitLabCI(config);
|
|
581
|
-
case 'jenkins':
|
|
582
|
-
return generateJenkinsfile(config);
|
|
583
|
-
default:
|
|
584
|
-
throw new CIError(
|
|
585
|
-
CI_ERROR_CODES.WORKFLOW_GENERATION_FAILED,
|
|
586
|
-
`Unsupported CI platform: ${platform}`,
|
|
587
|
-
{ supportedPlatforms: ['github', 'gitlab', 'jenkins'] }
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Write workflow file to appropriate location
|
|
594
|
-
* @param {string} content - Workflow content
|
|
595
|
-
* @param {string} platform - CI platform
|
|
596
|
-
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
597
|
-
* @returns {Promise<string>} Path to written file
|
|
598
|
-
*/
|
|
599
|
-
export async function writeWorkflowFile(content, platform, projectRoot = process.cwd()) {
|
|
600
|
-
let filePath;
|
|
601
|
-
|
|
602
|
-
switch (platform) {
|
|
603
|
-
case 'github':
|
|
604
|
-
filePath = path.join(projectRoot, '.github', 'workflows', 'agentful.yml');
|
|
605
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
606
|
-
break;
|
|
607
|
-
case 'gitlab':
|
|
608
|
-
filePath = path.join(projectRoot, '.gitlab-ci.yml');
|
|
609
|
-
break;
|
|
610
|
-
case 'jenkins':
|
|
611
|
-
filePath = path.join(projectRoot, 'Jenkinsfile');
|
|
612
|
-
break;
|
|
613
|
-
default:
|
|
614
|
-
throw new CIError(
|
|
615
|
-
CI_ERROR_CODES.WORKFLOW_GENERATION_FAILED,
|
|
616
|
-
`Cannot write workflow for unknown platform: ${platform}`
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
await fs.writeFile(filePath, content, 'utf-8');
|
|
621
|
-
return filePath;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Export all functions
|
|
625
|
-
export default {
|
|
626
|
-
loadAgentDefinition,
|
|
627
|
-
listAvailableAgents,
|
|
628
|
-
buildCIPrompt,
|
|
629
|
-
injectCIContext,
|
|
630
|
-
extractCIContext,
|
|
631
|
-
extractGitHubContext,
|
|
632
|
-
extractGitLabContext,
|
|
633
|
-
extractJenkinsContext,
|
|
634
|
-
generateWorkflow,
|
|
635
|
-
generateGitHubWorkflow,
|
|
636
|
-
generateGitLabCI,
|
|
637
|
-
generateJenkinsfile,
|
|
638
|
-
writeWorkflowFile,
|
|
639
|
-
CIError,
|
|
640
|
-
CI_ERROR_CODES,
|
|
641
|
-
};
|