agentic-forge 0.0.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/.gitattributes +24 -0
- package/.github/workflows/ci.yml +70 -0
- package/.markdownlint-cli2.jsonc +16 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/.vscode/agentic-forge.code-workspace +26 -0
- package/CHANGELOG.md +100 -0
- package/CLAUDE.md +158 -0
- package/CONTRIBUTING.md +152 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/agentic-forge-banner.png +0 -0
- package/biome.json +21 -0
- package/package.json +5 -0
- package/scripts/copy-assets.js +21 -0
- package/src/agents/explorer.md +97 -0
- package/src/agents/reviewer.md +137 -0
- package/src/checkpoints/manager.ts +119 -0
- package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
- package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
- package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
- package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
- package/src/claude/.claude/skills/analyze/references/security.md +76 -0
- package/src/claude/.claude/skills/analyze/references/style.md +72 -0
- package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
- package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
- package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
- package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
- package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
- package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
- package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
- package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
- package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
- package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
- package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
- package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
- package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
- package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
- package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
- package/src/cli.ts +182 -0
- package/src/commands/config-cmd.ts +28 -0
- package/src/commands/index.ts +21 -0
- package/src/commands/init.ts +96 -0
- package/src/commands/release-notes.ts +85 -0
- package/src/commands/resume.ts +103 -0
- package/src/commands/run.ts +234 -0
- package/src/commands/shortcuts.ts +11 -0
- package/src/commands/skills-dir.ts +11 -0
- package/src/commands/status.ts +112 -0
- package/src/commands/update.ts +64 -0
- package/src/commands/version.ts +27 -0
- package/src/commands/workflows.ts +129 -0
- package/src/config.ts +129 -0
- package/src/console.ts +790 -0
- package/src/executor.ts +354 -0
- package/src/git/worktree.ts +236 -0
- package/src/logging/logger.ts +95 -0
- package/src/orchestrator.ts +815 -0
- package/src/parser.ts +225 -0
- package/src/progress.ts +306 -0
- package/src/prompts/agentic-system.md +31 -0
- package/src/ralph-loop.ts +260 -0
- package/src/renderer.ts +164 -0
- package/src/runner.ts +634 -0
- package/src/signal-manager.ts +55 -0
- package/src/steps/base.ts +71 -0
- package/src/steps/conditional-step.ts +144 -0
- package/src/steps/index.ts +15 -0
- package/src/steps/parallel-step.ts +213 -0
- package/src/steps/prompt-step.ts +121 -0
- package/src/steps/ralph-loop-step.ts +186 -0
- package/src/steps/serial-step.ts +84 -0
- package/src/templates/analysis/bug.md.j2 +35 -0
- package/src/templates/analysis/debt.md.j2 +38 -0
- package/src/templates/analysis/doc.md.j2 +45 -0
- package/src/templates/analysis/security.md.j2 +35 -0
- package/src/templates/analysis/style.md.j2 +44 -0
- package/src/templates/analysis-summary.md.j2 +58 -0
- package/src/templates/checkpoint.md.j2 +27 -0
- package/src/templates/implementation-report.md.j2 +81 -0
- package/src/templates/memory.md.j2 +16 -0
- package/src/templates/plan-bug.md.j2 +42 -0
- package/src/templates/plan-chore.md.j2 +27 -0
- package/src/templates/plan-feature.md.j2 +41 -0
- package/src/templates/progress.json.j2 +16 -0
- package/src/templates/ralph-report.md.j2 +45 -0
- package/src/types.ts +141 -0
- package/src/workflows/analyze-codebase-merge.yaml +328 -0
- package/src/workflows/analyze-codebase.yaml +196 -0
- package/src/workflows/analyze-single.yaml +56 -0
- package/src/workflows/demo.yaml +180 -0
- package/src/workflows/one-shot.yaml +54 -0
- package/src/workflows/plan-build-review.yaml +160 -0
- package/src/workflows/ralph-loop.yaml +73 -0
- package/tests/config.test.ts +219 -0
- package/tests/console.test.ts +506 -0
- package/tests/executor.test.ts +339 -0
- package/tests/init.test.ts +86 -0
- package/tests/logger.test.ts +110 -0
- package/tests/parser.test.ts +290 -0
- package/tests/progress.test.ts +345 -0
- package/tests/ralph-loop.test.ts +418 -0
- package/tests/renderer.test.ts +350 -0
- package/tests/runner.test.ts +497 -0
- package/tests/setup.test.ts +7 -0
- package/tests/signal-manager.test.ts +26 -0
- package/tests/steps.test.ts +412 -0
- package/tests/worktree.test.ts +411 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/** Ralph Wiggum loop state management and completion detection. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
|
|
7
|
+
// --- Types ---
|
|
8
|
+
|
|
9
|
+
export interface RalphLoopState {
|
|
10
|
+
active: boolean;
|
|
11
|
+
iteration: number;
|
|
12
|
+
maxIterations: number;
|
|
13
|
+
completionPromise: string;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompletionResult {
|
|
19
|
+
isComplete: boolean;
|
|
20
|
+
promiseMatched: boolean;
|
|
21
|
+
promiseValue: string | null;
|
|
22
|
+
output: string;
|
|
23
|
+
isFailed: boolean;
|
|
24
|
+
failureReason: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- State path ---
|
|
28
|
+
|
|
29
|
+
export function getRalphStatePath(workflowId: string, stepName: string, repoRoot: string): string {
|
|
30
|
+
const safeName = stepName.replace(/[^\w-]/g, "_");
|
|
31
|
+
return path.join(repoRoot, "agentic", "outputs", workflowId, `ralph-${safeName}.md`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- State management ---
|
|
35
|
+
|
|
36
|
+
export function createRalphState(
|
|
37
|
+
workflowId: string,
|
|
38
|
+
stepName: string,
|
|
39
|
+
prompt: string,
|
|
40
|
+
maxIterations: number,
|
|
41
|
+
completionPromise: string,
|
|
42
|
+
repoRoot: string,
|
|
43
|
+
): RalphLoopState {
|
|
44
|
+
const state: RalphLoopState = {
|
|
45
|
+
active: true,
|
|
46
|
+
iteration: 1,
|
|
47
|
+
maxIterations,
|
|
48
|
+
completionPromise,
|
|
49
|
+
startedAt: new Date().toISOString(),
|
|
50
|
+
prompt,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
saveRalphState(workflowId, stepName, state, repoRoot);
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function loadRalphState(
|
|
58
|
+
workflowId: string,
|
|
59
|
+
stepName: string,
|
|
60
|
+
repoRoot: string,
|
|
61
|
+
): RalphLoopState | null {
|
|
62
|
+
const statePath = getRalphStatePath(workflowId, stepName, repoRoot);
|
|
63
|
+
if (!existsSync(statePath)) return null;
|
|
64
|
+
|
|
65
|
+
const content = readFileSync(statePath, "utf-8");
|
|
66
|
+
return parseRalphState(content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function updateRalphIteration(
|
|
70
|
+
workflowId: string,
|
|
71
|
+
stepName: string,
|
|
72
|
+
repoRoot: string,
|
|
73
|
+
): RalphLoopState | null {
|
|
74
|
+
const state = loadRalphState(workflowId, stepName, repoRoot);
|
|
75
|
+
if (!state) return null;
|
|
76
|
+
|
|
77
|
+
state.iteration += 1;
|
|
78
|
+
saveRalphState(workflowId, stepName, state, repoRoot);
|
|
79
|
+
return state;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function deactivateRalphState(workflowId: string, stepName: string, repoRoot: string): void {
|
|
83
|
+
const state = loadRalphState(workflowId, stepName, repoRoot);
|
|
84
|
+
if (state) {
|
|
85
|
+
state.active = false;
|
|
86
|
+
saveRalphState(workflowId, stepName, state, repoRoot);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function deleteRalphState(workflowId: string, stepName: string, repoRoot: string): void {
|
|
91
|
+
const statePath = getRalphStatePath(workflowId, stepName, repoRoot);
|
|
92
|
+
if (existsSync(statePath)) {
|
|
93
|
+
unlinkSync(statePath);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Internal helpers ---
|
|
98
|
+
|
|
99
|
+
function saveRalphState(
|
|
100
|
+
workflowId: string,
|
|
101
|
+
stepName: string,
|
|
102
|
+
state: RalphLoopState,
|
|
103
|
+
repoRoot: string,
|
|
104
|
+
): void {
|
|
105
|
+
const statePath = getRalphStatePath(workflowId, stepName, repoRoot);
|
|
106
|
+
const dir = path.dirname(statePath);
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const frontmatter = {
|
|
110
|
+
active: state.active,
|
|
111
|
+
iteration: state.iteration,
|
|
112
|
+
max_iterations: state.maxIterations,
|
|
113
|
+
completion_promise: state.completionPromise,
|
|
114
|
+
started_at: state.startedAt,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const yamlStr = yaml.dump(frontmatter, { flowLevel: -1 }).trim();
|
|
118
|
+
const content = `---
|
|
119
|
+
${yamlStr}
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# Ralph Loop Prompt
|
|
123
|
+
|
|
124
|
+
${state.prompt}
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
writeFileSync(statePath, content, "utf-8");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseRalphState(content: string): RalphLoopState | null {
|
|
131
|
+
if (!content.startsWith("---")) return null;
|
|
132
|
+
|
|
133
|
+
const parts = content.split("---");
|
|
134
|
+
if (parts.length < 3) return null;
|
|
135
|
+
|
|
136
|
+
let frontmatter: Record<string, unknown>;
|
|
137
|
+
try {
|
|
138
|
+
frontmatter = yaml.load(parts[1]) as Record<string, unknown>;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const promptSection = parts.slice(2).join("---").trim();
|
|
144
|
+
let prompt: string;
|
|
145
|
+
if (promptSection.includes("# Ralph Loop Prompt")) {
|
|
146
|
+
prompt = promptSection.split("# Ralph Loop Prompt")[1].trim();
|
|
147
|
+
} else {
|
|
148
|
+
prompt = promptSection;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
active: (frontmatter.active as boolean) ?? false,
|
|
153
|
+
iteration: (frontmatter.iteration as number) ?? 1,
|
|
154
|
+
maxIterations: (frontmatter.max_iterations as number) ?? 5,
|
|
155
|
+
completionPromise: (frontmatter.completion_promise as string) ?? "COMPLETE",
|
|
156
|
+
startedAt: (frontmatter.started_at as string) ?? "",
|
|
157
|
+
prompt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Completion detection ---
|
|
162
|
+
|
|
163
|
+
export function detectCompletionPromise(output: string, expectedPromise: string): CompletionResult {
|
|
164
|
+
const jsonPatterns = [
|
|
165
|
+
/```json\s*\n?(.*?)\n?```/gis,
|
|
166
|
+
/```\s*\n?(\{.*?\})\n?```/gis,
|
|
167
|
+
/(\{[^{}]*"ralph_complete"[^{}]*\})/gi,
|
|
168
|
+
/(\{[^{}]*"ralph_failed"[^{}]*\})/gi,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const pattern of jsonPatterns) {
|
|
172
|
+
pattern.lastIndex = 0;
|
|
173
|
+
const matches = [...output.matchAll(pattern)];
|
|
174
|
+
for (const match of matches) {
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(match[1].trim());
|
|
177
|
+
if (typeof data !== "object" || data === null) continue;
|
|
178
|
+
|
|
179
|
+
// Check for completion signal
|
|
180
|
+
if (data.ralph_complete) {
|
|
181
|
+
const promiseValue = String(data.promise ?? "");
|
|
182
|
+
const promiseMatched =
|
|
183
|
+
promiseValue.trim().toUpperCase() === expectedPromise.trim().toUpperCase();
|
|
184
|
+
return {
|
|
185
|
+
isComplete: true,
|
|
186
|
+
promiseMatched,
|
|
187
|
+
promiseValue,
|
|
188
|
+
output,
|
|
189
|
+
isFailed: false,
|
|
190
|
+
failureReason: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check for failure signal
|
|
195
|
+
if (data.ralph_failed) {
|
|
196
|
+
const failureReason = String(data.reason ?? "Unknown error");
|
|
197
|
+
return {
|
|
198
|
+
isComplete: true,
|
|
199
|
+
promiseMatched: false,
|
|
200
|
+
promiseValue: null,
|
|
201
|
+
output,
|
|
202
|
+
isFailed: true,
|
|
203
|
+
failureReason,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Invalid JSON, skip
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
isComplete: false,
|
|
214
|
+
promiseMatched: false,
|
|
215
|
+
promiseValue: null,
|
|
216
|
+
output,
|
|
217
|
+
isFailed: false,
|
|
218
|
+
failureReason: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- System message builder ---
|
|
223
|
+
|
|
224
|
+
export function buildRalphSystemMessage(
|
|
225
|
+
iteration: number,
|
|
226
|
+
maxIterations: number,
|
|
227
|
+
completionPromise: string,
|
|
228
|
+
): string {
|
|
229
|
+
return `## Ralph Wiggum Loop - Iteration ${iteration}/${maxIterations}
|
|
230
|
+
|
|
231
|
+
You are in a Ralph Wiggum iterative loop. Each iteration starts a fresh session.
|
|
232
|
+
|
|
233
|
+
**Completion Signal:**
|
|
234
|
+
When your task is FULLY complete, output a JSON block:
|
|
235
|
+
\`\`\`json
|
|
236
|
+
{
|
|
237
|
+
"ralph_complete": true,
|
|
238
|
+
"promise": "${completionPromise}"
|
|
239
|
+
}
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
**Failure Signal:**
|
|
243
|
+
If you encounter a BLOCKING error that prevents completion (e.g., missing permissions, cannot write files, access denied), output a failure JSON block:
|
|
244
|
+
\`\`\`json
|
|
245
|
+
{
|
|
246
|
+
"ralph_failed": true,
|
|
247
|
+
"reason": "<brief description of the blocking error>"
|
|
248
|
+
}
|
|
249
|
+
\`\`\`
|
|
250
|
+
|
|
251
|
+
**IMPORTANT:**
|
|
252
|
+
- Only output the completion JSON when the task is genuinely finished
|
|
253
|
+
- Do not output false promises to exit early
|
|
254
|
+
- Output the FAILURE JSON immediately if you encounter permission errors or other unrecoverable blocking issues - do NOT continue iterating
|
|
255
|
+
- If the task is incomplete but can still progress, continue working - you have ${maxIterations - iteration} iterations remaining
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
`;
|
|
260
|
+
}
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Nunjucks template rendering for workflows. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import nunjucks from "nunjucks";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
/** Set of variable names already warned about (reset per render). */
|
|
11
|
+
const warnedVars = new Set<string>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom nunjucks loader that warns on undefined variables instead of failing.
|
|
15
|
+
* Returns the original `{{ variable_name }}` syntax for undefined variables.
|
|
16
|
+
*/
|
|
17
|
+
function createLenientEnvironment(templateDirs: string[]): nunjucks.Environment {
|
|
18
|
+
const loader = new nunjucks.FileSystemLoader(templateDirs, { noCache: true });
|
|
19
|
+
const env = new nunjucks.Environment(loader, { autoescape: false });
|
|
20
|
+
|
|
21
|
+
// Install a global that intercepts missing variables
|
|
22
|
+
// Nunjucks doesn't have a direct WarnOnUndefined equivalent,
|
|
23
|
+
// so we use a custom extension approach
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createStrictEnvironment(templateDirs: string[]): nunjucks.Environment {
|
|
28
|
+
const loader = new nunjucks.FileSystemLoader(templateDirs, { noCache: true });
|
|
29
|
+
return new nunjucks.Environment(loader, {
|
|
30
|
+
autoescape: false,
|
|
31
|
+
throwOnUndefined: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TemplateRenderer {
|
|
36
|
+
templateDirs: string[];
|
|
37
|
+
strictMode: boolean;
|
|
38
|
+
private env: nunjucks.Environment;
|
|
39
|
+
|
|
40
|
+
constructor(templateDirs?: string[], strictMode = false) {
|
|
41
|
+
const dirs = templateDirs ?? [path.join(__dirname, "templates")];
|
|
42
|
+
|
|
43
|
+
this.templateDirs = dirs;
|
|
44
|
+
this.strictMode = strictMode;
|
|
45
|
+
|
|
46
|
+
const existingDirs = dirs.filter((d) => existsSync(d));
|
|
47
|
+
this.env = strictMode
|
|
48
|
+
? createStrictEnvironment(existingDirs)
|
|
49
|
+
: createLenientEnvironment(existingDirs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(templateName: string, context: Record<string, unknown>): string {
|
|
53
|
+
if (!this.strictMode) {
|
|
54
|
+
warnedVars.clear();
|
|
55
|
+
}
|
|
56
|
+
return this.env.render(templateName, context);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
renderString(templateStr: string, context: Record<string, unknown>): string {
|
|
60
|
+
if (!this.strictMode) {
|
|
61
|
+
warnedVars.clear();
|
|
62
|
+
}
|
|
63
|
+
return this.env.renderString(templateStr, context);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
hasVariables(text: string): boolean {
|
|
67
|
+
return text.includes("{{") || text.includes("{%");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addTemplateDir(dirPath: string): void {
|
|
71
|
+
if (existsSync(dirPath) && !this.templateDirs.includes(dirPath)) {
|
|
72
|
+
this.templateDirs.push(dirPath);
|
|
73
|
+
const existingDirs = this.templateDirs.filter((d) => existsSync(d));
|
|
74
|
+
this.env = this.strictMode
|
|
75
|
+
? createStrictEnvironment(existingDirs)
|
|
76
|
+
: createLenientEnvironment(existingDirs);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Helper functions ---
|
|
82
|
+
|
|
83
|
+
const ANALYSIS_TYPES = ["bug", "debt", "doc", "security", "style"];
|
|
84
|
+
|
|
85
|
+
export function extractAnalysisSteps(
|
|
86
|
+
stepOutputs: Record<string, Record<string, unknown>>,
|
|
87
|
+
): Record<string, Record<string, unknown>> {
|
|
88
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
89
|
+
for (const [name, data] of Object.entries(stepOutputs)) {
|
|
90
|
+
if (name.startsWith("analyze-")) {
|
|
91
|
+
const analysisType = name.slice("analyze-".length);
|
|
92
|
+
if (ANALYSIS_TYPES.includes(analysisType)) {
|
|
93
|
+
result[name] = data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function extractFixSteps(
|
|
101
|
+
stepOutputs: Record<string, Record<string, unknown>>,
|
|
102
|
+
): Record<string, Record<string, unknown>> {
|
|
103
|
+
const result: Record<string, Record<string, unknown>> = {};
|
|
104
|
+
for (const [name, data] of Object.entries(stepOutputs)) {
|
|
105
|
+
if (name.startsWith("fix-") || name.startsWith("apply-")) {
|
|
106
|
+
result[name] = data;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildTemplateContext(
|
|
113
|
+
workflowName: string,
|
|
114
|
+
workflowId: string,
|
|
115
|
+
startedAt: string,
|
|
116
|
+
completedAt: string | null,
|
|
117
|
+
stepOutputs: Record<string, Record<string, unknown>>,
|
|
118
|
+
filesChanged: string[],
|
|
119
|
+
branches: string[],
|
|
120
|
+
pullRequests: Record<string, unknown>[],
|
|
121
|
+
inputs: Record<string, unknown>,
|
|
122
|
+
): Record<string, unknown> {
|
|
123
|
+
const analysisSteps = extractAnalysisSteps(stepOutputs);
|
|
124
|
+
const fixSteps = extractFixSteps(stepOutputs);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
workflow: {
|
|
128
|
+
name: workflowName,
|
|
129
|
+
id: workflowId,
|
|
130
|
+
started_at: startedAt,
|
|
131
|
+
completed_at: completedAt,
|
|
132
|
+
},
|
|
133
|
+
workflow_id: workflowId,
|
|
134
|
+
steps: stepOutputs,
|
|
135
|
+
analysis_steps: analysisSteps,
|
|
136
|
+
fix_steps: fixSteps,
|
|
137
|
+
files_changed: filesChanged,
|
|
138
|
+
branches,
|
|
139
|
+
pull_requests: pullRequests,
|
|
140
|
+
inputs,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderWorkflowOutput(
|
|
145
|
+
templatePath: string,
|
|
146
|
+
outputPath: string,
|
|
147
|
+
context: Record<string, unknown>,
|
|
148
|
+
templateDirs?: string[],
|
|
149
|
+
strictMode = false,
|
|
150
|
+
): void {
|
|
151
|
+
const renderer = new TemplateRenderer(templateDirs, strictMode);
|
|
152
|
+
|
|
153
|
+
let rendered: string;
|
|
154
|
+
if (path.isAbsolute(templatePath)) {
|
|
155
|
+
const content = readFileSync(templatePath, "utf-8");
|
|
156
|
+
rendered = renderer.renderString(content, context);
|
|
157
|
+
} else {
|
|
158
|
+
rendered = renderer.render(templatePath, context);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const dir = path.dirname(outputPath);
|
|
162
|
+
mkdirSync(dir, { recursive: true });
|
|
163
|
+
writeFileSync(outputPath, rendered, "utf-8");
|
|
164
|
+
}
|