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,815 @@
|
|
|
1
|
+
/** Async workflow orchestration with Claude decision loop. */
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import yaml from "js-yaml";
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { ConsoleOutput, OutputLevel, extractSummary } from "./console.js";
|
|
10
|
+
import { WorkflowExecutor } from "./executor.js";
|
|
11
|
+
import { type Worktree, createWorktree, pruneOrphaned, removeWorktree } from "./git/worktree.js";
|
|
12
|
+
import { WorkflowLogger } from "./logging/logger.js";
|
|
13
|
+
import {
|
|
14
|
+
WORKFLOW_STATUS,
|
|
15
|
+
createProgress,
|
|
16
|
+
generateWorkflowId,
|
|
17
|
+
loadProgress,
|
|
18
|
+
progressToDict,
|
|
19
|
+
saveProgress,
|
|
20
|
+
updateStepCompleted,
|
|
21
|
+
updateStepFailed,
|
|
22
|
+
updateStepStarted,
|
|
23
|
+
} from "./progress.js";
|
|
24
|
+
import {
|
|
25
|
+
type RalphLoopState,
|
|
26
|
+
buildRalphSystemMessage,
|
|
27
|
+
createRalphState,
|
|
28
|
+
deactivateRalphState,
|
|
29
|
+
detectCompletionPromise,
|
|
30
|
+
loadRalphState,
|
|
31
|
+
updateRalphIteration,
|
|
32
|
+
} from "./ralph-loop.js";
|
|
33
|
+
import { TemplateRenderer } from "./renderer.js";
|
|
34
|
+
import { runClaude } from "./runner.js";
|
|
35
|
+
import { SignalManager, handleGracefulShutdown } from "./signal-manager.js";
|
|
36
|
+
import type {
|
|
37
|
+
ParallelBranch,
|
|
38
|
+
StepDefinition,
|
|
39
|
+
StepProgress,
|
|
40
|
+
WorkflowDefinition,
|
|
41
|
+
WorkflowProgress,
|
|
42
|
+
} from "./types.js";
|
|
43
|
+
|
|
44
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
|
|
46
|
+
// --- Data types ---
|
|
47
|
+
|
|
48
|
+
export interface OrchestratorAction {
|
|
49
|
+
type: string;
|
|
50
|
+
stepName?: string | null;
|
|
51
|
+
contextToPass?: string | null;
|
|
52
|
+
errorContext?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OrchestratorDecision {
|
|
56
|
+
workflowStatus: string;
|
|
57
|
+
action: OrchestratorAction;
|
|
58
|
+
reasoning: string;
|
|
59
|
+
progressUpdate: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Main orchestrator ---
|
|
63
|
+
|
|
64
|
+
export class WorkflowOrchestrator {
|
|
65
|
+
repoRoot: string;
|
|
66
|
+
config: Record<string, unknown>;
|
|
67
|
+
renderer: TemplateRenderer;
|
|
68
|
+
executor: WorkflowExecutor;
|
|
69
|
+
private _runningProcesses: unknown[] = [];
|
|
70
|
+
private _signalManager: SignalManager;
|
|
71
|
+
|
|
72
|
+
constructor(repoRoot?: string) {
|
|
73
|
+
this.repoRoot = repoRoot ?? process.cwd();
|
|
74
|
+
this.config = loadConfig(this.repoRoot);
|
|
75
|
+
this.renderer = new TemplateRenderer();
|
|
76
|
+
this.executor = new WorkflowExecutor(this.repoRoot);
|
|
77
|
+
this._signalManager = new SignalManager();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private get _shutdownRequested(): boolean {
|
|
81
|
+
return this._signalManager.shutdownRequested;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private resolveModel(stepModel?: string | null): string {
|
|
85
|
+
if (stepModel) return stepModel;
|
|
86
|
+
const defaults = this.config.defaults as Record<string, unknown> | undefined;
|
|
87
|
+
return (defaults?.model as string) ?? "sonnet";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async run(
|
|
91
|
+
workflow: WorkflowDefinition,
|
|
92
|
+
variables?: Record<string, unknown> | null,
|
|
93
|
+
fromStep?: string | null,
|
|
94
|
+
terminalOutput = "base",
|
|
95
|
+
workflowFile = "",
|
|
96
|
+
resumeProgress?: WorkflowProgress | null,
|
|
97
|
+
): Promise<WorkflowProgress> {
|
|
98
|
+
pruneOrphaned(this.repoRoot);
|
|
99
|
+
|
|
100
|
+
const vars = variables ? { ...variables } : {};
|
|
101
|
+
|
|
102
|
+
const outputLevel = terminalOutput === "all" ? OutputLevel.ALL : OutputLevel.BASE;
|
|
103
|
+
const console = new ConsoleOutput(outputLevel);
|
|
104
|
+
|
|
105
|
+
let progress: WorkflowProgress;
|
|
106
|
+
|
|
107
|
+
if (resumeProgress) {
|
|
108
|
+
progress = resumeProgress;
|
|
109
|
+
const merged = { ...progress.variables };
|
|
110
|
+
Object.assign(merged, vars);
|
|
111
|
+
progress.variables = merged;
|
|
112
|
+
if (!progress.workflowFile && workflowFile) {
|
|
113
|
+
progress.workflowFile = workflowFile;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
progress = this.initProgress(workflow, vars, fromStep ?? null, workflowFile);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const logger = new WorkflowLogger(progress.workflowId, this.repoRoot);
|
|
120
|
+
logger.info("orchestrator", `Starting workflow: ${workflow.name}`);
|
|
121
|
+
console.workflowStart(workflow.name, progress.workflowId);
|
|
122
|
+
|
|
123
|
+
while (!this._shutdownRequested) {
|
|
124
|
+
const decision = await this.getOrchestratorDecision(workflow, progress, logger);
|
|
125
|
+
|
|
126
|
+
if (!decision) {
|
|
127
|
+
logger.error("orchestrator", "Failed to get orchestrator decision");
|
|
128
|
+
console.error("Failed to get orchestrator decision");
|
|
129
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
logger.info("orchestrator", decision.reasoning);
|
|
134
|
+
|
|
135
|
+
if (decision.workflowStatus === "completed") {
|
|
136
|
+
progress.status = WORKFLOW_STATUS.COMPLETED;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (decision.workflowStatus === "failed") {
|
|
140
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (decision.workflowStatus === "blocked") {
|
|
144
|
+
progress.status = WORKFLOW_STATUS.PAUSED;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (decision.action.type === "execute_step") {
|
|
149
|
+
await this.executeStepAction(workflow, progress, decision.action, logger, console);
|
|
150
|
+
} else if (decision.action.type === "retry_step") {
|
|
151
|
+
await this.retryStepAction(workflow, progress, decision.action, logger, console);
|
|
152
|
+
} else if (decision.action.type === "wait_for_human") {
|
|
153
|
+
this.waitForHumanAction(progress, decision.action, logger);
|
|
154
|
+
break;
|
|
155
|
+
} else if (decision.action.type === "abort") {
|
|
156
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
saveProgress(progress, this.repoRoot);
|
|
161
|
+
|
|
162
|
+
if (this._shutdownRequested) {
|
|
163
|
+
await this.handleShutdown(progress, logger);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
progress.completedAt = new Date().toISOString();
|
|
169
|
+
saveProgress(progress, this.repoRoot);
|
|
170
|
+
|
|
171
|
+
console.workflowComplete(workflow.name, progress.status);
|
|
172
|
+
return progress;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private initProgress(
|
|
176
|
+
workflow: WorkflowDefinition,
|
|
177
|
+
variables: Record<string, unknown>,
|
|
178
|
+
fromStep: string | null,
|
|
179
|
+
workflowFile = "",
|
|
180
|
+
): WorkflowProgress {
|
|
181
|
+
const workflowId = generateWorkflowId(workflow.name);
|
|
182
|
+
const stepNames = this.collectStepNames(workflow.steps);
|
|
183
|
+
|
|
184
|
+
for (const v of workflow.variables) {
|
|
185
|
+
if (!(v.name in variables)) {
|
|
186
|
+
if (v.required && v.default === undefined) {
|
|
187
|
+
throw new Error(`Missing required variable: ${v.name}`);
|
|
188
|
+
}
|
|
189
|
+
variables[v.name] = v.default;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const progress = createProgress(workflowId, workflow.name, stepNames, variables, workflowFile);
|
|
194
|
+
|
|
195
|
+
if (fromStep) {
|
|
196
|
+
let skip = true;
|
|
197
|
+
const newPending: string[] = [];
|
|
198
|
+
for (const name of stepNames) {
|
|
199
|
+
if (name === fromStep) {
|
|
200
|
+
skip = false;
|
|
201
|
+
}
|
|
202
|
+
if (skip) {
|
|
203
|
+
progress.completedSteps.push({
|
|
204
|
+
name,
|
|
205
|
+
status: "skipped",
|
|
206
|
+
startedAt: null,
|
|
207
|
+
completedAt: null,
|
|
208
|
+
retryCount: 0,
|
|
209
|
+
outputSummary: "Skipped (resumed from later step)",
|
|
210
|
+
error: null,
|
|
211
|
+
humanInput: null,
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
newPending.push(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
progress.pendingSteps = newPending;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
saveProgress(progress, this.repoRoot);
|
|
221
|
+
return progress;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private collectStepNames(steps: StepDefinition[]): string[] {
|
|
225
|
+
const names: string[] = [];
|
|
226
|
+
for (const step of steps) {
|
|
227
|
+
names.push(step.name);
|
|
228
|
+
if (step.steps && step.steps.length > 0) {
|
|
229
|
+
names.push(...this.collectStepNames(step.steps));
|
|
230
|
+
}
|
|
231
|
+
if (step.thenSteps && step.thenSteps.length > 0) {
|
|
232
|
+
names.push(...this.collectStepNames(step.thenSteps));
|
|
233
|
+
}
|
|
234
|
+
if (step.elseSteps && step.elseSteps.length > 0) {
|
|
235
|
+
names.push(...this.collectStepNames(step.elseSteps));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return names;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async getOrchestratorDecision(
|
|
242
|
+
workflow: WorkflowDefinition,
|
|
243
|
+
progress: WorkflowProgress,
|
|
244
|
+
logger: WorkflowLogger,
|
|
245
|
+
): Promise<OrchestratorDecision | null> {
|
|
246
|
+
const cmdPath = path.join(__dirname, "..", "commands", "orchestrate.md");
|
|
247
|
+
let cmdTemplate: string;
|
|
248
|
+
try {
|
|
249
|
+
cmdTemplate = readFileSync(cmdPath, "utf-8");
|
|
250
|
+
} catch {
|
|
251
|
+
logger.error("orchestrator", "Orchestrate command not found");
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const workflowYaml = yaml.dump(this.workflowToDict(workflow));
|
|
256
|
+
const progressJson = JSON.stringify(progressToDict(progress), null, 2);
|
|
257
|
+
|
|
258
|
+
let lastStepName: string | null = null;
|
|
259
|
+
let lastStepOutput: unknown = null;
|
|
260
|
+
if (progress.completedSteps.length > 0) {
|
|
261
|
+
const last = progress.completedSteps[progress.completedSteps.length - 1];
|
|
262
|
+
lastStepName = last.name;
|
|
263
|
+
lastStepOutput = progress.stepOutputs[lastStepName] ?? "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const prompt = this.renderer.renderString(cmdTemplate, {
|
|
267
|
+
workflow_yaml: workflowYaml,
|
|
268
|
+
progress_json: progressJson,
|
|
269
|
+
last_step_name: lastStepName,
|
|
270
|
+
last_step_output: lastStepOutput ? String(lastStepOutput).slice(0, 2000) : null,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const defaults = this.config.defaults as Record<string, unknown> | undefined;
|
|
274
|
+
const maxRetry = (defaults?.maxRetry as number) ?? 3;
|
|
275
|
+
|
|
276
|
+
for (let attempt = 0; attempt < maxRetry; attempt++) {
|
|
277
|
+
const result = await runClaude({
|
|
278
|
+
prompt,
|
|
279
|
+
cwd: this.repoRoot,
|
|
280
|
+
model: "sonnet",
|
|
281
|
+
timeout: 120,
|
|
282
|
+
printOutput: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!result.success) {
|
|
286
|
+
logger.warning("orchestrator", `Orchestrator call failed: ${result.stderr}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
return this.parseOrchestratorResponse(result.stdout);
|
|
292
|
+
} catch (e: unknown) {
|
|
293
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
294
|
+
logger.warning("orchestrator", `Failed to parse response: ${errorMsg}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private parseOrchestratorResponse(output: string): OrchestratorDecision {
|
|
302
|
+
let jsonStr = output;
|
|
303
|
+
if (output.includes("```json")) {
|
|
304
|
+
const start = output.indexOf("```json") + 7;
|
|
305
|
+
const end = output.indexOf("```", start);
|
|
306
|
+
jsonStr = output.slice(start, end).trim();
|
|
307
|
+
} else if (output.includes("```")) {
|
|
308
|
+
const start = output.indexOf("```") + 3;
|
|
309
|
+
const end = output.indexOf("```", start);
|
|
310
|
+
jsonStr = output.slice(start, end).trim();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const data = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
314
|
+
|
|
315
|
+
const actionData = (data.next_action as Record<string, unknown>) ?? {};
|
|
316
|
+
const action: OrchestratorAction = {
|
|
317
|
+
type: (actionData.type as string) ?? "abort",
|
|
318
|
+
stepName: (actionData.step_name as string) ?? null,
|
|
319
|
+
contextToPass: (actionData.context_to_pass as string) ?? null,
|
|
320
|
+
errorContext: (actionData.error_context as string) ?? null,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
workflowStatus: (data.workflow_status as string) ?? "failed",
|
|
325
|
+
action,
|
|
326
|
+
reasoning: (data.reasoning as string) ?? "",
|
|
327
|
+
progressUpdate: (data.progress_update as string) ?? "",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async executeStepAction(
|
|
332
|
+
workflow: WorkflowDefinition,
|
|
333
|
+
progress: WorkflowProgress,
|
|
334
|
+
action: OrchestratorAction,
|
|
335
|
+
logger: WorkflowLogger,
|
|
336
|
+
console: ConsoleOutput,
|
|
337
|
+
): Promise<void> {
|
|
338
|
+
const step = this.findStep(workflow.steps, action.stepName ?? null);
|
|
339
|
+
if (!step) {
|
|
340
|
+
logger.error("orchestrator", `Step not found: ${action.stepName}`);
|
|
341
|
+
console.error(`Step not found: ${action.stepName}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (step.type === "parallel") {
|
|
346
|
+
await this.executeParallelStep(workflow, step, progress, logger, console);
|
|
347
|
+
} else if (step.type === "conditional") {
|
|
348
|
+
await this.executeConditionalStep(workflow, step, progress, logger, console);
|
|
349
|
+
} else if (step.type === "ralph-loop") {
|
|
350
|
+
await this.executeRalphLoopStep(workflow, step, progress, logger, console);
|
|
351
|
+
} else if (step.type === "wait-for-human") {
|
|
352
|
+
this.executeWaitForHumanStep(step, progress, logger);
|
|
353
|
+
} else {
|
|
354
|
+
await this.executor.executeStep(step, progress, progress.variables, logger, console);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private async executeParallelStep(
|
|
359
|
+
workflow: WorkflowDefinition,
|
|
360
|
+
step: StepDefinition,
|
|
361
|
+
progress: WorkflowProgress,
|
|
362
|
+
logger: WorkflowLogger,
|
|
363
|
+
console: ConsoleOutput,
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
logger.info(step.name, `Starting parallel execution with ${step.steps.length} branches`);
|
|
366
|
+
console.stepStart(step.name, "parallel");
|
|
367
|
+
console.info(`Parallel execution with ${step.steps.length} branches`);
|
|
368
|
+
updateStepStarted(progress, step.name);
|
|
369
|
+
|
|
370
|
+
const execution = this.config.execution as Record<string, unknown> | undefined;
|
|
371
|
+
const maxWorkers = (execution?.maxWorkers as number) ?? 4;
|
|
372
|
+
const worktrees: Worktree[] = [];
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
for (const subStep of step.steps) {
|
|
376
|
+
const wt = createWorktree(workflow.name, subStep.name, this.repoRoot);
|
|
377
|
+
worktrees.push(wt);
|
|
378
|
+
|
|
379
|
+
const branch: ParallelBranch = {
|
|
380
|
+
branchId: subStep.name,
|
|
381
|
+
status: "running",
|
|
382
|
+
worktreePath: wt.path,
|
|
383
|
+
progressFile: path.join(
|
|
384
|
+
wt.path,
|
|
385
|
+
"agentic",
|
|
386
|
+
"outputs",
|
|
387
|
+
progress.workflowId,
|
|
388
|
+
"progress.json",
|
|
389
|
+
),
|
|
390
|
+
};
|
|
391
|
+
progress.parallelBranches.push(branch);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
saveProgress(progress, this.repoRoot);
|
|
395
|
+
|
|
396
|
+
// Execute branches concurrently with concurrency cap
|
|
397
|
+
const results: {
|
|
398
|
+
success: boolean;
|
|
399
|
+
subStep: StepDefinition;
|
|
400
|
+
wt: Worktree;
|
|
401
|
+
error?: unknown;
|
|
402
|
+
}[] = [];
|
|
403
|
+
const pending = worktrees.map((wt, i) => ({ wt, i }));
|
|
404
|
+
let idx = 0;
|
|
405
|
+
|
|
406
|
+
const runNext = async (): Promise<void> => {
|
|
407
|
+
while (idx < pending.length) {
|
|
408
|
+
const current = pending[idx++];
|
|
409
|
+
const { wt, i } = current;
|
|
410
|
+
try {
|
|
411
|
+
const success = await this.executeInWorktree(
|
|
412
|
+
workflow,
|
|
413
|
+
step.steps[i],
|
|
414
|
+
progress,
|
|
415
|
+
wt,
|
|
416
|
+
logger,
|
|
417
|
+
console,
|
|
418
|
+
);
|
|
419
|
+
results.push({ success, subStep: step.steps[i], wt });
|
|
420
|
+
} catch (e) {
|
|
421
|
+
results.push({ success: false, subStep: step.steps[i], wt, error: e });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const workers = Array.from({ length: Math.min(maxWorkers, pending.length) }, () => runNext());
|
|
427
|
+
await Promise.all(workers);
|
|
428
|
+
let allSuccess = true;
|
|
429
|
+
|
|
430
|
+
for (const result of results) {
|
|
431
|
+
if (!result.success) {
|
|
432
|
+
allSuccess = false;
|
|
433
|
+
const errorMsg = (result as { error?: Error }).error?.message ?? "Parallel branch failed";
|
|
434
|
+
logger.error(result.subStep.name, errorMsg);
|
|
435
|
+
console.stepFailed(result.subStep.name, errorMsg);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (allSuccess) {
|
|
440
|
+
updateStepCompleted(progress, step.name, "All parallel branches completed");
|
|
441
|
+
console.stepComplete(step.name, "All parallel branches completed");
|
|
442
|
+
logger.info(step.name, "Parallel execution completed successfully");
|
|
443
|
+
} else {
|
|
444
|
+
updateStepFailed(progress, step.name, "One or more parallel branches failed");
|
|
445
|
+
console.stepFailed(step.name, "One or more parallel branches failed");
|
|
446
|
+
}
|
|
447
|
+
} finally {
|
|
448
|
+
for (const wt of worktrees) {
|
|
449
|
+
try {
|
|
450
|
+
removeWorktree(wt, this.repoRoot, false);
|
|
451
|
+
} catch (e: unknown) {
|
|
452
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
453
|
+
logger.warning(step.name, `Failed to clean worktree: ${errorMsg}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
progress.parallelBranches = [];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async executeInWorktree(
|
|
461
|
+
workflow: WorkflowDefinition,
|
|
462
|
+
step: StepDefinition,
|
|
463
|
+
parentProgress: WorkflowProgress,
|
|
464
|
+
worktree: Worktree,
|
|
465
|
+
logger: WorkflowLogger,
|
|
466
|
+
console: ConsoleOutput,
|
|
467
|
+
): Promise<boolean> {
|
|
468
|
+
const wtExecutor = new WorkflowExecutor(worktree.path);
|
|
469
|
+
const wtLogger = new WorkflowLogger(parentProgress.workflowId, worktree.path);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
await wtExecutor.executeStep(
|
|
473
|
+
step,
|
|
474
|
+
parentProgress,
|
|
475
|
+
parentProgress.variables,
|
|
476
|
+
wtLogger,
|
|
477
|
+
console,
|
|
478
|
+
);
|
|
479
|
+
return true;
|
|
480
|
+
} catch (e: unknown) {
|
|
481
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
482
|
+
logger.error(step.name, `Worktree execution failed: ${errorMsg}`);
|
|
483
|
+
console.stepFailed(step.name, `Worktree execution failed: ${errorMsg}`);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private async executeConditionalStep(
|
|
489
|
+
workflow: WorkflowDefinition,
|
|
490
|
+
step: StepDefinition,
|
|
491
|
+
progress: WorkflowProgress,
|
|
492
|
+
logger: WorkflowLogger,
|
|
493
|
+
console: ConsoleOutput,
|
|
494
|
+
): Promise<void> {
|
|
495
|
+
logger.info(step.name, `Evaluating condition: ${step.condition}`);
|
|
496
|
+
console.stepStart(step.name, "conditional");
|
|
497
|
+
updateStepStarted(progress, step.name);
|
|
498
|
+
|
|
499
|
+
const context = { outputs: progress.stepOutputs, variables: progress.variables };
|
|
500
|
+
let isTrue: boolean;
|
|
501
|
+
try {
|
|
502
|
+
const conditionResult = this.renderer.renderString(
|
|
503
|
+
`{{ ${step.condition ?? "false"} }}`,
|
|
504
|
+
context,
|
|
505
|
+
);
|
|
506
|
+
isTrue = ["true", "1", "yes"].includes(conditionResult.toLowerCase());
|
|
507
|
+
} catch (e: unknown) {
|
|
508
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
509
|
+
logger.error(step.name, `Condition evaluation failed: ${errorMsg}`);
|
|
510
|
+
console.stepFailed(step.name, `Condition evaluation failed: ${errorMsg}`);
|
|
511
|
+
updateStepFailed(progress, step.name, `Condition error: ${errorMsg}`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const stepsToRun = isTrue ? step.thenSteps : step.elseSteps;
|
|
516
|
+
const branchName = isTrue ? "then" : "else";
|
|
517
|
+
logger.info(
|
|
518
|
+
step.name,
|
|
519
|
+
`Condition ${isTrue ? "met" : "not met"}, executing ${stepsToRun.length} steps`,
|
|
520
|
+
);
|
|
521
|
+
console.info(`Condition ${branchName} branch: executing ${stepsToRun.length} steps`);
|
|
522
|
+
|
|
523
|
+
for (const subStep of stepsToRun) {
|
|
524
|
+
await this.executor.executeStep(subStep, progress, progress.variables, logger, console);
|
|
525
|
+
if (progress.status === WORKFLOW_STATUS.FAILED) {
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (progress.status !== WORKFLOW_STATUS.FAILED) {
|
|
531
|
+
updateStepCompleted(progress, step.name, `Condition: ${isTrue}`);
|
|
532
|
+
console.stepComplete(step.name, `Condition ${branchName} branch completed`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private async executeRalphLoopStep(
|
|
537
|
+
workflow: WorkflowDefinition,
|
|
538
|
+
step: StepDefinition,
|
|
539
|
+
progress: WorkflowProgress,
|
|
540
|
+
logger: WorkflowLogger,
|
|
541
|
+
console: ConsoleOutput,
|
|
542
|
+
): Promise<void> {
|
|
543
|
+
const completionPromise = step.completionPromise ?? "COMPLETE";
|
|
544
|
+
const maxIterations =
|
|
545
|
+
typeof step.maxIterations === "string"
|
|
546
|
+
? Number.parseInt(step.maxIterations, 10)
|
|
547
|
+
: step.maxIterations;
|
|
548
|
+
|
|
549
|
+
const resolvedModel = this.resolveModel(step.model);
|
|
550
|
+
|
|
551
|
+
logger.info(
|
|
552
|
+
step.name,
|
|
553
|
+
`Starting Ralph loop (max ${maxIterations} iterations, promise: ${completionPromise})`,
|
|
554
|
+
);
|
|
555
|
+
console.stepStart(step.name, "ralph-loop", resolvedModel);
|
|
556
|
+
console.info(`Ralph loop starting (max ${maxIterations} iterations)`);
|
|
557
|
+
updateStepStarted(progress, step.name);
|
|
558
|
+
|
|
559
|
+
// Render prompt template
|
|
560
|
+
const templateContext = {
|
|
561
|
+
variables: progress.variables,
|
|
562
|
+
outputs: progress.stepOutputs,
|
|
563
|
+
...progress.variables,
|
|
564
|
+
};
|
|
565
|
+
let prompt = step.prompt ?? "";
|
|
566
|
+
if (this.renderer.hasVariables(prompt)) {
|
|
567
|
+
prompt = this.renderer.renderString(prompt, templateContext);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check for existing state to resume
|
|
571
|
+
const existingState = loadRalphState(progress.workflowId, step.name, this.repoRoot);
|
|
572
|
+
let state: RalphLoopState;
|
|
573
|
+
if (existingState?.active) {
|
|
574
|
+
state = existingState;
|
|
575
|
+
logger.info(step.name, `Resuming Ralph loop from iteration ${state.iteration}`);
|
|
576
|
+
console.info(`Resuming Ralph loop from iteration ${state.iteration}`);
|
|
577
|
+
} else {
|
|
578
|
+
state = createRalphState(
|
|
579
|
+
progress.workflowId,
|
|
580
|
+
step.name,
|
|
581
|
+
prompt,
|
|
582
|
+
maxIterations,
|
|
583
|
+
completionPromise,
|
|
584
|
+
this.repoRoot,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const printOutput = console.level === OutputLevel.ALL;
|
|
589
|
+
let finalOutput = "";
|
|
590
|
+
let completed = false;
|
|
591
|
+
|
|
592
|
+
while (state.iteration <= maxIterations && !this._shutdownRequested) {
|
|
593
|
+
logger.info(step.name, `Ralph iteration ${state.iteration}/${maxIterations}`);
|
|
594
|
+
|
|
595
|
+
const ralphMessage = buildRalphSystemMessage(
|
|
596
|
+
state.iteration,
|
|
597
|
+
maxIterations,
|
|
598
|
+
completionPromise,
|
|
599
|
+
);
|
|
600
|
+
const fullPrompt = ralphMessage + prompt;
|
|
601
|
+
|
|
602
|
+
console.ralphIterationStart(step.name, state.iteration, maxIterations);
|
|
603
|
+
|
|
604
|
+
const timeout = (step.stepTimeoutMinutes ?? 60) * 60;
|
|
605
|
+
const result = await runClaude({
|
|
606
|
+
prompt: fullPrompt,
|
|
607
|
+
cwd: this.repoRoot,
|
|
608
|
+
model: this.resolveModel(step.model),
|
|
609
|
+
timeout,
|
|
610
|
+
printOutput,
|
|
611
|
+
skipPermissions: true,
|
|
612
|
+
console,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
const errorSummary = result.stderr ? extractSummary(result.stderr) : "Unknown error";
|
|
617
|
+
logger.warning(step.name, `Iteration ${state.iteration} failed: ${result.stderr}`);
|
|
618
|
+
console.ralphIteration(
|
|
619
|
+
step.name,
|
|
620
|
+
state.iteration,
|
|
621
|
+
maxIterations,
|
|
622
|
+
`Failed: ${errorSummary}`,
|
|
623
|
+
);
|
|
624
|
+
const newState = updateRalphIteration(progress.workflowId, step.name, this.repoRoot);
|
|
625
|
+
if (!newState) break;
|
|
626
|
+
state = newState;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const iterationSummary = extractSummary(result.stdout);
|
|
631
|
+
console.ralphIteration(step.name, state.iteration, maxIterations, iterationSummary);
|
|
632
|
+
|
|
633
|
+
const completionResult = detectCompletionPromise(result.stdout, completionPromise);
|
|
634
|
+
|
|
635
|
+
if (completionResult.isComplete && completionResult.promiseMatched) {
|
|
636
|
+
logger.info(step.name, `Completion promise matched after ${state.iteration} iterations`);
|
|
637
|
+
finalOutput = result.stdout;
|
|
638
|
+
completed = true;
|
|
639
|
+
deactivateRalphState(progress.workflowId, step.name, this.repoRoot);
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (completionResult.isComplete && !completionResult.promiseMatched) {
|
|
644
|
+
logger.warning(
|
|
645
|
+
step.name,
|
|
646
|
+
`Completion signaled but promise mismatch: got '${completionResult.promiseValue}', expected '${completionPromise}'`,
|
|
647
|
+
);
|
|
648
|
+
console.warning(
|
|
649
|
+
`Promise mismatch: got '${completionResult.promiseValue}', expected '${completionPromise}'`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const newState = updateRalphIteration(progress.workflowId, step.name, this.repoRoot);
|
|
654
|
+
if (!newState) {
|
|
655
|
+
logger.error(step.name, "Failed to update Ralph state");
|
|
656
|
+
console.error("Failed to update Ralph state");
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
state = newState;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (completed) {
|
|
663
|
+
updateStepCompleted(
|
|
664
|
+
progress,
|
|
665
|
+
step.name,
|
|
666
|
+
`Completed after ${state.iteration} iterations`,
|
|
667
|
+
finalOutput,
|
|
668
|
+
);
|
|
669
|
+
console.ralphComplete(step.name, state.iteration, maxIterations);
|
|
670
|
+
} else if (this._shutdownRequested) {
|
|
671
|
+
logger.info(step.name, "Ralph loop interrupted by shutdown");
|
|
672
|
+
console.warning("Ralph loop interrupted by shutdown");
|
|
673
|
+
updateStepFailed(progress, step.name, "Interrupted by shutdown");
|
|
674
|
+
} else {
|
|
675
|
+
logger.warning(step.name, `Max iterations (${maxIterations}) reached without completion`);
|
|
676
|
+
deactivateRalphState(progress.workflowId, step.name, this.repoRoot);
|
|
677
|
+
updateStepFailed(
|
|
678
|
+
progress,
|
|
679
|
+
step.name,
|
|
680
|
+
`Max iterations (${maxIterations}) reached without completion promise`,
|
|
681
|
+
);
|
|
682
|
+
console.ralphMaxIterations(step.name, maxIterations);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private executeWaitForHumanStep(
|
|
687
|
+
step: StepDefinition,
|
|
688
|
+
progress: WorkflowProgress,
|
|
689
|
+
logger: WorkflowLogger,
|
|
690
|
+
): void {
|
|
691
|
+
logger.info(step.name, `Waiting for human input: ${step.message}`);
|
|
692
|
+
updateStepStarted(progress, step.name);
|
|
693
|
+
|
|
694
|
+
progress.currentStep = {
|
|
695
|
+
name: step.name,
|
|
696
|
+
type: "wait-for-human",
|
|
697
|
+
message: step.message,
|
|
698
|
+
started_at: new Date().toISOString(),
|
|
699
|
+
timeout_minutes: step.stepTimeoutMinutes ?? 5,
|
|
700
|
+
on_timeout: step.onTimeout,
|
|
701
|
+
};
|
|
702
|
+
progress.status = WORKFLOW_STATUS.PAUSED;
|
|
703
|
+
|
|
704
|
+
const sep = "=".repeat(60);
|
|
705
|
+
process.stdout.write(`\n${sep}\n`);
|
|
706
|
+
process.stdout.write("HUMAN INPUT REQUIRED\n");
|
|
707
|
+
process.stdout.write(`${sep}\n`);
|
|
708
|
+
process.stdout.write(`\n${step.message}\n\n`);
|
|
709
|
+
process.stdout.write(
|
|
710
|
+
`Provide input with: agentic-forge input ${progress.workflowId} "<your response>"\n`,
|
|
711
|
+
);
|
|
712
|
+
process.stdout.write(`${sep}\n\n`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private async retryStepAction(
|
|
716
|
+
workflow: WorkflowDefinition,
|
|
717
|
+
progress: WorkflowProgress,
|
|
718
|
+
action: OrchestratorAction,
|
|
719
|
+
logger: WorkflowLogger,
|
|
720
|
+
console: ConsoleOutput,
|
|
721
|
+
): Promise<void> {
|
|
722
|
+
let step = this.findStep(workflow.steps, action.stepName ?? null);
|
|
723
|
+
if (!step) {
|
|
724
|
+
logger.error("orchestrator", `Step not found: ${action.stepName}`);
|
|
725
|
+
console.error(`Step not found: ${action.stepName}`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
console.info(`Retrying step: ${action.stepName}`);
|
|
730
|
+
|
|
731
|
+
if (action.errorContext && step.prompt) {
|
|
732
|
+
step = {
|
|
733
|
+
...step,
|
|
734
|
+
prompt: `${step.prompt}\n\nPrevious attempt failed:\n${action.errorContext}`,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
await this.executor.executeStep(step, progress, progress.variables, logger, console);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private waitForHumanAction(
|
|
742
|
+
progress: WorkflowProgress,
|
|
743
|
+
action: OrchestratorAction,
|
|
744
|
+
logger: WorkflowLogger,
|
|
745
|
+
): void {
|
|
746
|
+
progress.status = WORKFLOW_STATUS.PAUSED;
|
|
747
|
+
logger.info("orchestrator", "Workflow paused waiting for human input");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private findStep(steps: StepDefinition[], name: string | null): StepDefinition | null {
|
|
751
|
+
if (!name) return null;
|
|
752
|
+
for (const step of steps) {
|
|
753
|
+
if (step.name === name) return step;
|
|
754
|
+
if (step.steps?.length) {
|
|
755
|
+
const found = this.findStep(step.steps, name);
|
|
756
|
+
if (found) return found;
|
|
757
|
+
}
|
|
758
|
+
if (step.thenSteps?.length) {
|
|
759
|
+
const found = this.findStep(step.thenSteps, name);
|
|
760
|
+
if (found) return found;
|
|
761
|
+
}
|
|
762
|
+
if (step.elseSteps?.length) {
|
|
763
|
+
const found = this.findStep(step.elseSteps, name);
|
|
764
|
+
if (found) return found;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private async handleShutdown(progress: WorkflowProgress, logger: WorkflowLogger): Promise<void> {
|
|
771
|
+
await handleGracefulShutdown(progress, logger, this.repoRoot);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private workflowToDict(workflow: WorkflowDefinition): Record<string, unknown> {
|
|
775
|
+
return {
|
|
776
|
+
name: workflow.name,
|
|
777
|
+
steps: workflow.steps.map((s) => ({ name: s.name, type: s.type })),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// --- Process human input ---
|
|
783
|
+
|
|
784
|
+
export function processHumanInput(
|
|
785
|
+
workflowId: string,
|
|
786
|
+
response: string,
|
|
787
|
+
repoRoot?: string,
|
|
788
|
+
): boolean {
|
|
789
|
+
const root = repoRoot ?? process.cwd();
|
|
790
|
+
const progress = loadProgress(workflowId, root);
|
|
791
|
+
|
|
792
|
+
if (!progress) {
|
|
793
|
+
process.stdout.write(`Workflow not found: ${workflowId}\n`);
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (progress.status !== WORKFLOW_STATUS.PAUSED) {
|
|
798
|
+
process.stdout.write(`Workflow is not paused: ${progress.status}\n`);
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (!progress.currentStep || progress.currentStep.type !== "wait-for-human") {
|
|
803
|
+
process.stdout.write("Workflow is not waiting for human input\n");
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
progress.currentStep.human_input = response;
|
|
808
|
+
progress.status = WORKFLOW_STATUS.RUNNING;
|
|
809
|
+
saveProgress(progress, root);
|
|
810
|
+
|
|
811
|
+
process.stdout.write(
|
|
812
|
+
`Input received. Resume workflow with: agentic-forge resume ${workflowId}\n`,
|
|
813
|
+
);
|
|
814
|
+
return true;
|
|
815
|
+
}
|