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,71 @@
|
|
|
1
|
+
/** Base class and types for step executors. */
|
|
2
|
+
|
|
3
|
+
import type { ConsoleOutput } from "../console.js";
|
|
4
|
+
import type { WorkflowLogger } from "../logging/logger.js";
|
|
5
|
+
import type { TemplateRenderer } from "../renderer.js";
|
|
6
|
+
import type { StepDefinition, WorkflowProgress, WorkflowSettings } from "../types.js";
|
|
7
|
+
|
|
8
|
+
// --- Step context ---
|
|
9
|
+
|
|
10
|
+
export interface StepContext {
|
|
11
|
+
repoRoot: string;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
renderer: TemplateRenderer;
|
|
14
|
+
workflowSettings: WorkflowSettings | null;
|
|
15
|
+
workflowId: string;
|
|
16
|
+
variables: Record<string, unknown>;
|
|
17
|
+
outputs: Record<string, unknown>;
|
|
18
|
+
cwdOverride?: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildTemplateContext(context: StepContext): Record<string, unknown> {
|
|
22
|
+
return {
|
|
23
|
+
variables: context.variables,
|
|
24
|
+
outputs: context.outputs,
|
|
25
|
+
workflow_id: context.workflowId,
|
|
26
|
+
...(context.variables as Record<string, unknown>),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveModel(context: StepContext, stepModel?: string | null): string {
|
|
31
|
+
if (stepModel) {
|
|
32
|
+
return stepModel;
|
|
33
|
+
}
|
|
34
|
+
if (context.workflowSettings?.model) {
|
|
35
|
+
return context.workflowSettings.model;
|
|
36
|
+
}
|
|
37
|
+
const defaults = context.config.defaults as Record<string, unknown> | undefined;
|
|
38
|
+
return (defaults?.model as string) ?? "sonnet";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Step result ---
|
|
42
|
+
|
|
43
|
+
export interface StepResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
outputSummary?: string;
|
|
46
|
+
fullOutput?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Abstract base ---
|
|
51
|
+
|
|
52
|
+
export abstract class StepExecutor {
|
|
53
|
+
abstract execute(
|
|
54
|
+
step: StepDefinition,
|
|
55
|
+
progress: WorkflowProgress,
|
|
56
|
+
context: StepContext,
|
|
57
|
+
logger: WorkflowLogger,
|
|
58
|
+
console: ConsoleOutput,
|
|
59
|
+
): Promise<StepResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Branch executor type ---
|
|
63
|
+
|
|
64
|
+
/** Callable type for executing a single step within a composite executor. */
|
|
65
|
+
export type BranchStepExecutor = (
|
|
66
|
+
step: StepDefinition,
|
|
67
|
+
progress: WorkflowProgress,
|
|
68
|
+
context: StepContext,
|
|
69
|
+
logger: WorkflowLogger,
|
|
70
|
+
console: ConsoleOutput,
|
|
71
|
+
) => Promise<StepResult>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** Conditional step executor. */
|
|
2
|
+
|
|
3
|
+
import type { ConsoleOutput } from "../console.js";
|
|
4
|
+
import type { WorkflowLogger } from "../logging/logger.js";
|
|
5
|
+
import { WORKFLOW_STATUS, updateStepCompleted, updateStepFailed } from "../progress.js";
|
|
6
|
+
import type { StepDefinition, WorkflowProgress } from "../types.js";
|
|
7
|
+
import {
|
|
8
|
+
type BranchStepExecutor,
|
|
9
|
+
type StepContext,
|
|
10
|
+
StepExecutor,
|
|
11
|
+
type StepResult,
|
|
12
|
+
buildTemplateContext,
|
|
13
|
+
} from "./base.js";
|
|
14
|
+
|
|
15
|
+
export class ConditionalStepExecutor extends StepExecutor {
|
|
16
|
+
constructor(private branchExecutor: BranchStepExecutor) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async execute(
|
|
21
|
+
step: StepDefinition,
|
|
22
|
+
progress: WorkflowProgress,
|
|
23
|
+
context: StepContext,
|
|
24
|
+
logger: WorkflowLogger,
|
|
25
|
+
console: ConsoleOutput,
|
|
26
|
+
): Promise<StepResult> {
|
|
27
|
+
let condition = step.condition ?? "false";
|
|
28
|
+
logger.info(step.name, `Evaluating condition: ${condition}`);
|
|
29
|
+
|
|
30
|
+
const templateContext = buildTemplateContext(context);
|
|
31
|
+
if (context.renderer.hasVariables(condition)) {
|
|
32
|
+
condition = context.renderer.renderString(condition, templateContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const conditionResult = this.evaluateCondition(condition, templateContext);
|
|
36
|
+
logger.info(step.name, `Condition evaluated to: ${conditionResult}`);
|
|
37
|
+
console.info(`Conditional '${step.name}': ${condition} = ${conditionResult}`);
|
|
38
|
+
|
|
39
|
+
const stepsToRun = conditionResult ? step.thenSteps : step.elseSteps;
|
|
40
|
+
const branchName = conditionResult ? "then" : "else";
|
|
41
|
+
|
|
42
|
+
if (!stepsToRun || stepsToRun.length === 0) {
|
|
43
|
+
const outputSummary = `Condition ${conditionResult}, no '${branchName}' branch to execute`;
|
|
44
|
+
updateStepCompleted(progress, step.name, outputSummary);
|
|
45
|
+
console.stepComplete(step.name, outputSummary);
|
|
46
|
+
return { success: true, outputSummary };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.info(step.name, `Executing '${branchName}' branch with ${stepsToRun.length} steps`);
|
|
50
|
+
|
|
51
|
+
for (const subStep of stepsToRun) {
|
|
52
|
+
try {
|
|
53
|
+
const result = await this.branchExecutor(subStep, progress, context, logger, console);
|
|
54
|
+
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
logger.warning(step.name, `Conditional '${branchName}' branch stopped due to failure`);
|
|
57
|
+
const errorMsg = `Step '${subStep.name}' failed: ${result.error}`;
|
|
58
|
+
updateStepFailed(progress, step.name, errorMsg);
|
|
59
|
+
console.stepFailed(step.name, `Step '${subStep.name}' failed`);
|
|
60
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
61
|
+
return { success: false, error: errorMsg };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (progress.status === WORKFLOW_STATUS.FAILED) {
|
|
65
|
+
logger.warning(step.name, `Conditional '${branchName}' branch stopped due to failure`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
const errStr = e instanceof Error ? e.message : String(e);
|
|
70
|
+
logger.error(subStep.name, `Step failed in conditional branch: ${errStr}`);
|
|
71
|
+
const errorMsg = `Step '${subStep.name}' failed: ${errStr}`;
|
|
72
|
+
updateStepFailed(progress, step.name, errorMsg);
|
|
73
|
+
console.stepFailed(step.name, `Step '${subStep.name}' failed`);
|
|
74
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
75
|
+
return { success: false, error: errorMsg };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (progress.status !== WORKFLOW_STATUS.FAILED) {
|
|
80
|
+
const outputSummary = `Executed '${branchName}' branch (${stepsToRun.length} steps)`;
|
|
81
|
+
updateStepCompleted(progress, step.name, outputSummary);
|
|
82
|
+
console.stepComplete(step.name, outputSummary);
|
|
83
|
+
logger.info(step.name, outputSummary);
|
|
84
|
+
return { success: true, outputSummary };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { success: false, error: "Conditional execution failed" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
evaluateCondition(condition: string, context: Record<string, unknown>): boolean {
|
|
91
|
+
const trimmed = condition.trim();
|
|
92
|
+
|
|
93
|
+
if (["true", "1", "yes"].includes(trimmed.toLowerCase())) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (["false", "0", "no", "none", ""].includes(trimmed.toLowerCase())) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (trimmed.includes("!=")) {
|
|
101
|
+
const [left, right] = trimmed.split("!=", 2);
|
|
102
|
+
const leftVal = this.resolveValue(left.trim(), context);
|
|
103
|
+
const rightVal = this.resolveValue(right.trim(), context);
|
|
104
|
+
return leftVal !== rightVal;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (trimmed.includes("==")) {
|
|
108
|
+
const [left, right] = trimmed.split("==", 2);
|
|
109
|
+
const leftVal = this.resolveValue(left.trim(), context);
|
|
110
|
+
const rightVal = this.resolveValue(right.trim(), context);
|
|
111
|
+
return leftVal === rightVal;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const value = this.resolveValue(trimmed, context);
|
|
115
|
+
return (
|
|
116
|
+
Boolean(value) && value !== "none" && value !== "None" && value !== "null" && value !== ""
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
resolveValue(expr: string, context: Record<string, unknown>): unknown {
|
|
121
|
+
const trimmed = expr.trim();
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
125
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
|
126
|
+
) {
|
|
127
|
+
return trimmed.slice(1, -1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (trimmed.startsWith("variables.")) {
|
|
131
|
+
const varName = trimmed.slice(10);
|
|
132
|
+
const vars = context.variables as Record<string, unknown> | undefined;
|
|
133
|
+
return vars?.[varName];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (trimmed.startsWith("outputs.")) {
|
|
137
|
+
const outputName = trimmed.slice(8);
|
|
138
|
+
const outputs = context.outputs as Record<string, unknown> | undefined;
|
|
139
|
+
return outputs?.[outputName];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (context as Record<string, unknown>)[trimmed] ?? trimmed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Step executors for workflow execution. */
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
StepContext,
|
|
5
|
+
StepResult,
|
|
6
|
+
StepExecutor,
|
|
7
|
+
BranchStepExecutor,
|
|
8
|
+
buildTemplateContext,
|
|
9
|
+
resolveModel,
|
|
10
|
+
} from "./base.js";
|
|
11
|
+
export { PromptStepExecutor } from "./prompt-step.js";
|
|
12
|
+
export { SerialStepExecutor } from "./serial-step.js";
|
|
13
|
+
export { ConditionalStepExecutor } from "./conditional-step.js";
|
|
14
|
+
export { RalphLoopStepExecutor } from "./ralph-loop-step.js";
|
|
15
|
+
export { ParallelStepExecutor } from "./parallel-step.js";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/** Parallel step executor. */
|
|
2
|
+
|
|
3
|
+
import type { ConsoleOutput } from "../console.js";
|
|
4
|
+
import { type Worktree, createWorktree, removeWorktree, runGit } from "../git/worktree.js";
|
|
5
|
+
import type { WorkflowLogger } from "../logging/logger.js";
|
|
6
|
+
import { WORKFLOW_STATUS, updateStepCompleted, updateStepFailed } from "../progress.js";
|
|
7
|
+
import type { StepDefinition, WorkflowProgress } from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
type BranchStepExecutor,
|
|
10
|
+
type StepContext,
|
|
11
|
+
StepExecutor,
|
|
12
|
+
type StepResult,
|
|
13
|
+
} from "./base.js";
|
|
14
|
+
|
|
15
|
+
export class ParallelStepExecutor extends StepExecutor {
|
|
16
|
+
constructor(private branchExecutor: BranchStepExecutor) {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async execute(
|
|
21
|
+
step: StepDefinition,
|
|
22
|
+
progress: WorkflowProgress,
|
|
23
|
+
context: StepContext,
|
|
24
|
+
logger: WorkflowLogger,
|
|
25
|
+
console: ConsoleOutput,
|
|
26
|
+
): Promise<StepResult> {
|
|
27
|
+
if (!step.steps || step.steps.length === 0) {
|
|
28
|
+
logger.warning(step.name, "Parallel step has no sub-steps");
|
|
29
|
+
updateStepCompleted(progress, step.name, "No sub-steps to execute");
|
|
30
|
+
console.stepComplete(step.name, "No sub-steps to execute");
|
|
31
|
+
return { success: true, outputSummary: "No sub-steps to execute" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const useWorktree = step.git?.worktree ?? false;
|
|
35
|
+
|
|
36
|
+
logger.info(step.name, `Starting parallel execution of ${step.steps.length} branches`);
|
|
37
|
+
if (useWorktree) {
|
|
38
|
+
console.info(`Parallel: starting ${step.steps.length} branches (worktree isolation)`);
|
|
39
|
+
} else {
|
|
40
|
+
console.info(`Parallel: starting ${step.steps.length} branches`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Register branches for parallel display
|
|
44
|
+
const branchNames = step.steps.map((s) => s.name);
|
|
45
|
+
console.registerParallelBranches(branchNames);
|
|
46
|
+
console.enterParallelMode();
|
|
47
|
+
|
|
48
|
+
const branchResults: Record<string, { success: boolean; output: string }> = {};
|
|
49
|
+
const failedBranches: string[] = [];
|
|
50
|
+
const worktrees: Record<string, Worktree> = {};
|
|
51
|
+
|
|
52
|
+
const executeBranch = async (
|
|
53
|
+
branchStep: StepDefinition,
|
|
54
|
+
): Promise<[string, boolean, string, Worktree | null]> => {
|
|
55
|
+
const branchContext: StepContext = {
|
|
56
|
+
...context,
|
|
57
|
+
variables: { ...context.variables },
|
|
58
|
+
};
|
|
59
|
+
let worktree: Worktree | null = null;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
console.setParallelBranch(branchStep.name);
|
|
63
|
+
|
|
64
|
+
if (useWorktree) {
|
|
65
|
+
worktree = createWorktree(
|
|
66
|
+
progress.workflowName,
|
|
67
|
+
branchStep.name,
|
|
68
|
+
undefined,
|
|
69
|
+
context.repoRoot,
|
|
70
|
+
);
|
|
71
|
+
logger.info(branchStep.name, `Created worktree: ${worktree.path}`);
|
|
72
|
+
console.info(` Branch '${branchStep.name}' worktree: ${worktree.branch}`);
|
|
73
|
+
branchContext.cwdOverride = worktree.path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const result = await this.branchExecutor(
|
|
77
|
+
branchStep,
|
|
78
|
+
progress,
|
|
79
|
+
branchContext,
|
|
80
|
+
logger,
|
|
81
|
+
console,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
console.flushParallelBranch(branchStep.name);
|
|
85
|
+
|
|
86
|
+
return [branchStep.name, result.success, result.outputSummary ?? "", worktree];
|
|
87
|
+
} catch (e) {
|
|
88
|
+
const errStr = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
89
|
+
logger.error(branchStep.name, `Branch failed with exception: ${errStr}`);
|
|
90
|
+
console.flushParallelBranch(branchStep.name);
|
|
91
|
+
return [branchStep.name, false, String(e), worktree];
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Execute branches with concurrency cap from config
|
|
96
|
+
const execution = context.config?.execution as Record<string, unknown> | undefined;
|
|
97
|
+
const maxWorkers = (execution?.maxWorkers as number) ?? 4;
|
|
98
|
+
const settled: PromiseSettledResult<[string, boolean, string, Worktree | null]>[] = [];
|
|
99
|
+
const branches = [...step.steps];
|
|
100
|
+
let idx = 0;
|
|
101
|
+
|
|
102
|
+
const runNext = async (): Promise<void> => {
|
|
103
|
+
while (idx < branches.length) {
|
|
104
|
+
const branchStep = branches[idx++];
|
|
105
|
+
try {
|
|
106
|
+
const value = await executeBranch(branchStep);
|
|
107
|
+
settled.push({ status: "fulfilled", value });
|
|
108
|
+
} catch (reason) {
|
|
109
|
+
settled.push({ status: "rejected", reason });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const workers = Array.from({ length: Math.min(maxWorkers, branches.length) }, () => runNext());
|
|
115
|
+
await Promise.all(workers);
|
|
116
|
+
const results = settled;
|
|
117
|
+
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
if (result.status === "fulfilled") {
|
|
120
|
+
const [name, success, output, worktree] = result.value;
|
|
121
|
+
branchResults[name] = { success, output };
|
|
122
|
+
if (worktree) {
|
|
123
|
+
worktrees[name] = worktree;
|
|
124
|
+
}
|
|
125
|
+
if (success) {
|
|
126
|
+
console.info(` Branch '${name}' completed`);
|
|
127
|
+
} else {
|
|
128
|
+
console.error(` Branch '${name}' failed: ${output}`);
|
|
129
|
+
failedBranches.push(name);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Promise rejected — should be rare since executeBranch catches errors
|
|
133
|
+
const reason = String(result.reason);
|
|
134
|
+
// We can't easily get the branch name from a rejected promise
|
|
135
|
+
// but this case is already handled by the try/catch inside executeBranch
|
|
136
|
+
logger.error(step.name, `Branch promise rejected: ${reason}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Exit parallel mode
|
|
141
|
+
console.exitParallelMode();
|
|
142
|
+
|
|
143
|
+
// Handle merge modes
|
|
144
|
+
if (step.mergeMode === "merge" && Object.keys(worktrees).length > 0) {
|
|
145
|
+
this.mergeWorktreeBranches(step, worktrees, failedBranches, context, logger, console);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (step.mergeMode === "independent" && Object.keys(worktrees).length > 0) {
|
|
149
|
+
for (const [name, worktree] of Object.entries(worktrees)) {
|
|
150
|
+
removeWorktree(worktree, context.repoRoot, false);
|
|
151
|
+
logger.info(name, `Worktree removed, branch preserved: ${worktree.branch}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle completion based on merge strategy
|
|
156
|
+
if (step.mergeStrategy === "wait-all") {
|
|
157
|
+
if (failedBranches.length > 0 && step.mergeMode !== "independent") {
|
|
158
|
+
const errorMsg = `Parallel branches failed: ${failedBranches.join(", ")}`;
|
|
159
|
+
updateStepFailed(progress, step.name, errorMsg);
|
|
160
|
+
console.stepFailed(step.name, errorMsg);
|
|
161
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
162
|
+
return { success: false, error: errorMsg };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const completed = step.steps.length - failedBranches.length;
|
|
166
|
+
let outputSummary = `Completed ${completed}/${step.steps.length} branches`;
|
|
167
|
+
if (failedBranches.length > 0) {
|
|
168
|
+
outputSummary += ` (failed: ${failedBranches.join(", ")})`;
|
|
169
|
+
}
|
|
170
|
+
updateStepCompleted(progress, step.name, outputSummary);
|
|
171
|
+
console.stepComplete(step.name, outputSummary);
|
|
172
|
+
logger.info(step.name, outputSummary);
|
|
173
|
+
return { success: true, outputSummary };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { success: true, outputSummary: "Parallel execution completed" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private mergeWorktreeBranches(
|
|
180
|
+
step: StepDefinition,
|
|
181
|
+
worktrees: Record<string, Worktree>,
|
|
182
|
+
failedBranches: string[],
|
|
183
|
+
context: StepContext,
|
|
184
|
+
logger: WorkflowLogger,
|
|
185
|
+
console: ConsoleOutput,
|
|
186
|
+
): void {
|
|
187
|
+
console.info("Merging parallel branches...");
|
|
188
|
+
|
|
189
|
+
for (const [name, worktree] of Object.entries(worktrees)) {
|
|
190
|
+
if (failedBranches.includes(name)) {
|
|
191
|
+
removeWorktree(worktree, context.repoRoot, true);
|
|
192
|
+
logger.info(name, "Failed branch worktree removed");
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
runGit(["checkout", worktree.baseBranch], context.repoRoot);
|
|
198
|
+
runGit(
|
|
199
|
+
["merge", "--no-ff", "-m", `Merge parallel branch: ${name}`, worktree.branch],
|
|
200
|
+
context.repoRoot,
|
|
201
|
+
);
|
|
202
|
+
logger.info(name, `Merged branch ${worktree.branch} into ${worktree.baseBranch}`);
|
|
203
|
+
console.info(` Merged '${name}'`);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
logger.error(name, `Merge failed: ${e}`);
|
|
206
|
+
console.error(` Merge failed for '${name}': ${e}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
removeWorktree(worktree, context.repoRoot, true);
|
|
210
|
+
logger.info(name, "Worktree and branch cleaned up");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Prompt step executor. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import type { ConsoleOutput } from "../console.js";
|
|
8
|
+
import { extractJson, extractSummary } from "../console.js";
|
|
9
|
+
import type { WorkflowLogger } from "../logging/logger.js";
|
|
10
|
+
import { WORKFLOW_STATUS, updateStepCompleted, updateStepFailed } from "../progress.js";
|
|
11
|
+
import { runClaude } from "../runner.js";
|
|
12
|
+
import type { StepDefinition, WorkflowProgress } from "../types.js";
|
|
13
|
+
import {
|
|
14
|
+
type StepContext,
|
|
15
|
+
StepExecutor,
|
|
16
|
+
type StepResult,
|
|
17
|
+
buildTemplateContext,
|
|
18
|
+
resolveModel,
|
|
19
|
+
} from "./base.js";
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
export class PromptStepExecutor extends StepExecutor {
|
|
24
|
+
async execute(
|
|
25
|
+
step: StepDefinition,
|
|
26
|
+
progress: WorkflowProgress,
|
|
27
|
+
context: StepContext,
|
|
28
|
+
logger: WorkflowLogger,
|
|
29
|
+
console: ConsoleOutput,
|
|
30
|
+
): Promise<StepResult> {
|
|
31
|
+
let prompt = step.prompt ?? "";
|
|
32
|
+
const templateContext = buildTemplateContext(context);
|
|
33
|
+
|
|
34
|
+
if (context.renderer.hasVariables(prompt)) {
|
|
35
|
+
prompt = context.renderer.renderString(prompt, templateContext);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cwd = context.cwdOverride ?? context.repoRoot;
|
|
39
|
+
|
|
40
|
+
// Load agent file if specified (fallback to bundled agents)
|
|
41
|
+
if (step.agent) {
|
|
42
|
+
let agentPath = path.join(context.repoRoot, step.agent);
|
|
43
|
+
if (!existsSync(agentPath)) {
|
|
44
|
+
agentPath = path.join(__dirname, "..", "agents", path.basename(step.agent));
|
|
45
|
+
}
|
|
46
|
+
if (existsSync(agentPath)) {
|
|
47
|
+
const agentContent = readFileSync(agentPath, "utf-8");
|
|
48
|
+
prompt = `${agentContent}\n\n${prompt}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const timeout = (step.stepTimeoutMinutes ?? 60) * 60;
|
|
53
|
+
const defaults = context.config.defaults as Record<string, unknown> | undefined;
|
|
54
|
+
const maxRetry = step.stepMaxRetry ?? (defaults?.maxRetry as number) ?? 3;
|
|
55
|
+
const bypassPermissions = context.workflowSettings?.bypassPermissions ?? false;
|
|
56
|
+
const allowedTools = context.workflowSettings?.requiredTools?.length
|
|
57
|
+
? context.workflowSettings.requiredTools
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
// Always enable streaming when console is provided
|
|
61
|
+
const printOutput = true;
|
|
62
|
+
|
|
63
|
+
for (let attempt = 0; attempt <= maxRetry; attempt++) {
|
|
64
|
+
const result = await runClaude({
|
|
65
|
+
prompt,
|
|
66
|
+
cwd,
|
|
67
|
+
model: resolveModel(context, step.model),
|
|
68
|
+
timeout,
|
|
69
|
+
printOutput,
|
|
70
|
+
skipPermissions: bypassPermissions,
|
|
71
|
+
allowedTools,
|
|
72
|
+
console,
|
|
73
|
+
workflowId: context.workflowId,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.success) {
|
|
77
|
+
// Use session output context if available, fall back to extracted summary
|
|
78
|
+
const sessionOut = result.sessionOutput;
|
|
79
|
+
let outputSummary: string;
|
|
80
|
+
if (sessionOut.isSuccess && sessionOut.context) {
|
|
81
|
+
outputSummary = sessionOut.context;
|
|
82
|
+
if (sessionOut.sessionId) {
|
|
83
|
+
logger.info(step.name, `Session ID: ${sessionOut.sessionId}`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
outputSummary = result.stdout ? extractSummary(result.stdout) : "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Try to extract structured JSON from skill output
|
|
90
|
+
const stepOutput = result.stdout ? extractJson(result.stdout) : null;
|
|
91
|
+
const outputValue = stepOutput ?? result.stdout;
|
|
92
|
+
|
|
93
|
+
updateStepCompleted(progress, step.name, outputSummary, outputValue);
|
|
94
|
+
console.stepComplete(step.name, outputSummary);
|
|
95
|
+
logger.info(step.name, "Step completed successfully");
|
|
96
|
+
return { success: true, outputSummary, fullOutput: result.stdout };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (attempt < maxRetry) {
|
|
100
|
+
const errorSummary = result.stderr ? extractSummary(result.stderr) : "Unknown error";
|
|
101
|
+
console.stepRetry(step.name, attempt + 1, maxRetry + 1, errorSummary);
|
|
102
|
+
logger.warning(step.name, `Attempt ${attempt + 1} failed, retrying...`, {
|
|
103
|
+
error: result.stderr,
|
|
104
|
+
});
|
|
105
|
+
if (progress.currentStep) {
|
|
106
|
+
(progress.currentStep as Record<string, unknown>).retry_count = attempt + 1;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
const errorMsg = result.stderr || "Step failed";
|
|
110
|
+
const errorSummary = errorMsg ? extractSummary(errorMsg) : "Unknown error";
|
|
111
|
+
console.stepFailed(step.name, errorSummary);
|
|
112
|
+
updateStepFailed(progress, step.name, errorMsg);
|
|
113
|
+
progress.status = WORKFLOW_STATUS.FAILED;
|
|
114
|
+
logger.error(step.name, `Step failed after ${maxRetry + 1} attempts`);
|
|
115
|
+
return { success: false, error: errorMsg };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { success: false, error: "Unexpected exit from retry loop" };
|
|
120
|
+
}
|
|
121
|
+
}
|