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,96 @@
|
|
|
1
|
+
/** Init and configure command handlers. */
|
|
2
|
+
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { getDefaultConfig, loadConfig } from "../config.js";
|
|
7
|
+
import { getBundledWorkflowsDir } from "./run.js";
|
|
8
|
+
|
|
9
|
+
export function cmdInit(options: { force?: boolean; listOnly?: boolean }): void {
|
|
10
|
+
const bundledDir = getBundledWorkflowsDir();
|
|
11
|
+
if (!existsSync(bundledDir)) {
|
|
12
|
+
process.stderr.write("Error: Bundled workflows directory not found.\n");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const bundledWorkflows = readdirSync(bundledDir)
|
|
17
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
18
|
+
.sort();
|
|
19
|
+
|
|
20
|
+
if (bundledWorkflows.length === 0) {
|
|
21
|
+
process.stderr.write("No bundled workflows found.\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// List only mode
|
|
26
|
+
if (options.listOnly) {
|
|
27
|
+
process.stdout.write("Available bundled workflows:\n\n");
|
|
28
|
+
for (const wf of bundledWorkflows) {
|
|
29
|
+
process.stdout.write(` ${wf}\n`);
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write("\nUse 'agentic-forge init' to copy these to agentic/workflows/\n");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Copy workflows to local directory
|
|
36
|
+
const targetDir = path.join(process.cwd(), "agentic", "workflows");
|
|
37
|
+
mkdirSync(targetDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const copied: string[] = [];
|
|
40
|
+
const skipped: string[] = [];
|
|
41
|
+
for (const wf of bundledWorkflows) {
|
|
42
|
+
const targetPath = path.join(targetDir, wf);
|
|
43
|
+
if (existsSync(targetPath) && !options.force) {
|
|
44
|
+
skipped.push(wf);
|
|
45
|
+
} else {
|
|
46
|
+
copyFileSync(path.join(bundledDir, wf), targetPath);
|
|
47
|
+
copied.push(wf);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (copied.length > 0) {
|
|
52
|
+
process.stdout.write(`Copied ${copied.length} workflow(s) to ${targetDir}/\n`);
|
|
53
|
+
for (const name of copied) {
|
|
54
|
+
process.stdout.write(` + ${name}\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (skipped.length > 0) {
|
|
59
|
+
process.stdout.write(`\nSkipped ${skipped.length} existing workflow(s):\n`);
|
|
60
|
+
for (const name of skipped) {
|
|
61
|
+
process.stdout.write(` - ${name}\n`);
|
|
62
|
+
}
|
|
63
|
+
process.stdout.write("\nUse --force to overwrite existing files.\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create config.json next to workflows
|
|
67
|
+
initConfig(path.join(process.cwd(), "agentic"), options.force ?? false);
|
|
68
|
+
|
|
69
|
+
if (copied.length > 0) {
|
|
70
|
+
process.stdout.write("\nYou can now run workflows with:\n");
|
|
71
|
+
process.stdout.write(" agentic-forge run agentic/workflows/<workflow>.yaml\n");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function initConfig(agenticDir: string, force: boolean): void {
|
|
76
|
+
const configPath = path.join(agenticDir, "config.json");
|
|
77
|
+
if (existsSync(configPath) && !force) {
|
|
78
|
+
process.stdout.write(`\nConfig already exists: ${configPath}\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
mkdirSync(agenticDir, { recursive: true });
|
|
83
|
+
const config = getDefaultConfig();
|
|
84
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
85
|
+
process.stdout.write(`\nCreated config: ${configPath}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function cmdConfigure(): void {
|
|
89
|
+
const config = loadConfig();
|
|
90
|
+
process.stdout.write("Agentic Workflows Configuration\n");
|
|
91
|
+
process.stdout.write(`${"=".repeat(40)}\n`);
|
|
92
|
+
process.stdout.write("\nCurrent settings:\n");
|
|
93
|
+
process.stdout.write(`${JSON.stringify(config, null, 2)}\n`);
|
|
94
|
+
process.stdout.write("\nUse 'agentic-forge config set <key> <value>' to modify settings.\n");
|
|
95
|
+
process.stdout.write("Example: agentic-forge config set defaults.maxRetry 5\n");
|
|
96
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Release notes command handler. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
export function cmdReleaseNotes(options: {
|
|
10
|
+
specificVersion?: string;
|
|
11
|
+
latest?: boolean;
|
|
12
|
+
}): void {
|
|
13
|
+
let changelogPath: string | null = null;
|
|
14
|
+
|
|
15
|
+
// Try to find CHANGELOG.md relative to the package
|
|
16
|
+
const candidate = path.join(__dirname, "..", "..", "CHANGELOG.md");
|
|
17
|
+
if (existsSync(candidate)) {
|
|
18
|
+
changelogPath = candidate;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: try current directory
|
|
22
|
+
if (changelogPath === null) {
|
|
23
|
+
const cwdCandidate = path.join(process.cwd(), "CHANGELOG.md");
|
|
24
|
+
if (existsSync(cwdCandidate)) {
|
|
25
|
+
changelogPath = cwdCandidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (changelogPath === null) {
|
|
30
|
+
process.stdout.write("CHANGELOG.md not found\n");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const content = readFileSync(changelogPath, "utf-8");
|
|
35
|
+
|
|
36
|
+
if (options.specificVersion) {
|
|
37
|
+
const section = extractVersionSection(content, options.specificVersion);
|
|
38
|
+
if (section) {
|
|
39
|
+
process.stdout.write(`${section.trim()}\n`);
|
|
40
|
+
} else {
|
|
41
|
+
process.stdout.write(`Version ${options.specificVersion} not found in CHANGELOG.md\n`);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.latest) {
|
|
47
|
+
const section = extractLatestVersion(content);
|
|
48
|
+
if (section) {
|
|
49
|
+
process.stdout.write(`${section.trim()}\n`);
|
|
50
|
+
} else {
|
|
51
|
+
process.stdout.write("No version information found in CHANGELOG.md\n");
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.stdout.write(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractVersionSection(content: string, version: string): string | null {
|
|
60
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
const pattern = new RegExp(`^## \\[${escaped}\\].*$`, "m");
|
|
62
|
+
const match = pattern.exec(content);
|
|
63
|
+
|
|
64
|
+
if (!match) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const start = match.index;
|
|
69
|
+
const nextPattern = /^## \[[\d.]+\]/m;
|
|
70
|
+
const nextMatch = nextPattern.exec(content.slice(match.index + match[0].length));
|
|
71
|
+
const end = nextMatch ? match.index + match[0].length + nextMatch.index : content.length;
|
|
72
|
+
|
|
73
|
+
return content.slice(start, end);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractLatestVersion(content: string): string | null {
|
|
77
|
+
const pattern = /^## \[([\d.]+)\].*$/m;
|
|
78
|
+
const match = pattern.exec(content);
|
|
79
|
+
|
|
80
|
+
if (!match) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return extractVersionSection(content, match[1]);
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Resume command handler. */
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { WORKFLOW_STATUS, loadProgress, prepareForResume, saveProgress } from "../progress.js";
|
|
7
|
+
import { discoverWorkflow } from "./run.js";
|
|
8
|
+
|
|
9
|
+
export async function cmdResume(options: {
|
|
10
|
+
workflowId: string;
|
|
11
|
+
terminalOutput?: string;
|
|
12
|
+
}): Promise<void> {
|
|
13
|
+
const { WorkflowExecutor } = await import("../executor.js");
|
|
14
|
+
const { WorkflowParser, WorkflowParseError } = await import("../parser.js");
|
|
15
|
+
|
|
16
|
+
const progress = loadProgress(options.workflowId);
|
|
17
|
+
if (progress === null) {
|
|
18
|
+
process.stderr.write(`Error: Workflow not found: ${options.workflowId}\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Only reject completed workflows
|
|
23
|
+
if (progress.status === WORKFLOW_STATUS.COMPLETED) {
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`Error: Cannot resume a completed workflow (status: '${progress.status}')\n`,
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve workflow YAML file
|
|
31
|
+
let workflowPath: string | null = null;
|
|
32
|
+
|
|
33
|
+
// Try stored workflow_file first
|
|
34
|
+
if (progress.workflowFile) {
|
|
35
|
+
if (existsSync(progress.workflowFile)) {
|
|
36
|
+
workflowPath = progress.workflowFile;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fall back to discovery by workflow name
|
|
41
|
+
if (workflowPath === null) {
|
|
42
|
+
const [discovered] = discoverWorkflow(progress.workflowName);
|
|
43
|
+
if (discovered !== null) {
|
|
44
|
+
workflowPath = discovered;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (workflowPath === null) {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`Error: Cannot find workflow file for '${progress.workflowName}'.\nProvide the workflow YAML at one of the standard locations or re-run with 'agentic-forge run <path>'.\n`,
|
|
51
|
+
);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse workflow
|
|
56
|
+
let workflow: import("../types.js").WorkflowDefinition;
|
|
57
|
+
try {
|
|
58
|
+
const parser = new WorkflowParser();
|
|
59
|
+
workflow = parser.parseFile(workflowPath);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e instanceof WorkflowParseError) {
|
|
62
|
+
process.stderr.write(`Error parsing workflow: ${e.message}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Normalize state for resume
|
|
69
|
+
prepareForResume(progress);
|
|
70
|
+
saveProgress(progress);
|
|
71
|
+
|
|
72
|
+
// Execute with resume
|
|
73
|
+
const executor = new WorkflowExecutor();
|
|
74
|
+
try {
|
|
75
|
+
let terminalOutput = "base";
|
|
76
|
+
if (options.terminalOutput != null) {
|
|
77
|
+
terminalOutput = options.terminalOutput;
|
|
78
|
+
} else if (workflow.settings?.terminalOutput) {
|
|
79
|
+
terminalOutput = workflow.settings.terminalOutput;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await executor.run(
|
|
83
|
+
workflow,
|
|
84
|
+
undefined,
|
|
85
|
+
null,
|
|
86
|
+
terminalOutput,
|
|
87
|
+
path.resolve(workflowPath),
|
|
88
|
+
progress,
|
|
89
|
+
);
|
|
90
|
+
process.stdout.write(`\nWorkflow ${result.status}: ${result.workflowId}\n`);
|
|
91
|
+
if (result.errors && result.errors.length > 0) {
|
|
92
|
+
process.stdout.write("\nErrors:\n");
|
|
93
|
+
for (const error of result.errors) {
|
|
94
|
+
process.stdout.write(
|
|
95
|
+
` - ${(error as Record<string, string>).step}: ${(error as Record<string, string>).error}\n`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
process.stderr.write(`Error running workflow: ${(e as Error).message}\n`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** Run and resume command handlers with workflow discovery. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
export function getBundledWorkflowsDir(): string {
|
|
11
|
+
return path.join(__dirname, "..", "workflows");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getUserWorkflowsDir(): string {
|
|
15
|
+
if (process.platform === "win32") {
|
|
16
|
+
const base = process.env.APPDATA ?? path.join(homedir(), "AppData", "Roaming");
|
|
17
|
+
return path.join(base, "agentic-forge", "workflows");
|
|
18
|
+
}
|
|
19
|
+
const base = process.env.XDG_CONFIG_HOME ?? path.join(homedir(), ".config");
|
|
20
|
+
return path.join(base, "agentic-forge", "workflows");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getProjectWorkflowsDir(): string {
|
|
24
|
+
return path.join(process.cwd(), "agentic", "workflows");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function discoverWorkflow(name: string): [string | null, string] {
|
|
28
|
+
const fileName = name.endsWith(".yaml") ? name : `${name}.yaml`;
|
|
29
|
+
|
|
30
|
+
const searchLocations: [string, string][] = [
|
|
31
|
+
[getProjectWorkflowsDir(), "project-local"],
|
|
32
|
+
[getUserWorkflowsDir(), "user-global"],
|
|
33
|
+
[getBundledWorkflowsDir(), "bundled"],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const [directory, locationType] of searchLocations) {
|
|
37
|
+
const workflowPath = path.join(directory, fileName);
|
|
38
|
+
if (existsSync(workflowPath)) {
|
|
39
|
+
return [workflowPath, locationType];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [null, "not found"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function listAvailableWorkflows(): [string, string, string][] {
|
|
47
|
+
const workflows: [string, string, string][] = [];
|
|
48
|
+
|
|
49
|
+
const searchLocations: [string, string][] = [
|
|
50
|
+
[getProjectWorkflowsDir(), "project-local"],
|
|
51
|
+
[getUserWorkflowsDir(), "user-global"],
|
|
52
|
+
[getBundledWorkflowsDir(), "bundled"],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const [directory, locationType] of searchLocations) {
|
|
56
|
+
if (existsSync(directory)) {
|
|
57
|
+
const files = readdirSync(directory)
|
|
58
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
59
|
+
.sort();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const name = path.basename(file, ".yaml");
|
|
62
|
+
workflows.push([name, path.join(directory, file), locationType]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return workflows;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveWorkflowPath(workflowArg: string): [string, string] {
|
|
71
|
+
// Check if it's an absolute path
|
|
72
|
+
if (path.isAbsolute(workflowArg)) {
|
|
73
|
+
return [workflowArg, "absolute"];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if it exists as a relative path from cwd
|
|
77
|
+
const localPath = path.resolve(process.cwd(), workflowArg);
|
|
78
|
+
if (existsSync(localPath)) {
|
|
79
|
+
return [localPath, "relative"];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If the input looks like a bare name (no path separators), try discovery
|
|
83
|
+
if (!workflowArg.includes("/") && !workflowArg.includes("\\")) {
|
|
84
|
+
const [discovered, locationType] = discoverWorkflow(workflowArg);
|
|
85
|
+
if (discovered) {
|
|
86
|
+
return [discovered, locationType];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fallback: return the resolved path (will fail with appropriate error)
|
|
91
|
+
return [path.resolve(workflowArg), "not found"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function cmdRun(options: {
|
|
95
|
+
workflow?: string;
|
|
96
|
+
listWorkflows?: boolean;
|
|
97
|
+
vars?: string[];
|
|
98
|
+
fromStep?: string;
|
|
99
|
+
terminalOutput?: string;
|
|
100
|
+
}): Promise<void> {
|
|
101
|
+
// Handle --list flag
|
|
102
|
+
if (options.listWorkflows) {
|
|
103
|
+
process.stdout.write("Available workflows:\n\n");
|
|
104
|
+
const workflows = listAvailableWorkflows();
|
|
105
|
+
|
|
106
|
+
if (workflows.length === 0) {
|
|
107
|
+
process.stdout.write("No workflows found.\n");
|
|
108
|
+
process.stdout.write("\nSearched locations:\n");
|
|
109
|
+
process.stdout.write(` - Project: ${getProjectWorkflowsDir()}\n`);
|
|
110
|
+
process.stdout.write(` - User: ${getUserWorkflowsDir()}\n`);
|
|
111
|
+
process.stdout.write(` - Bundled: ${getBundledWorkflowsDir()}\n`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Group by location
|
|
116
|
+
const byLocation: Record<string, [string, string][]> = {};
|
|
117
|
+
for (const [name, wfPath, location] of workflows) {
|
|
118
|
+
if (!byLocation[location]) {
|
|
119
|
+
byLocation[location] = [];
|
|
120
|
+
}
|
|
121
|
+
byLocation[location].push([name, wfPath]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const location of ["project-local", "user-global", "bundled"]) {
|
|
125
|
+
if (byLocation[location]) {
|
|
126
|
+
const label = location.replace("-", " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
127
|
+
process.stdout.write(`${label}:\n`);
|
|
128
|
+
for (const [name] of byLocation[location]) {
|
|
129
|
+
process.stdout.write(` ${name}\n`);
|
|
130
|
+
}
|
|
131
|
+
process.stdout.write("\n");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.stdout.write(`Total: ${workflows.length} workflow(s)\n`);
|
|
136
|
+
process.stdout.write("\nUsage: agentic-forge run <workflow-name>\n");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate workflow argument is provided
|
|
141
|
+
if (!options.workflow) {
|
|
142
|
+
process.stderr.write("Error: workflow name or path is required\n");
|
|
143
|
+
process.stderr.write("Use 'agentic-forge run --list' to see available workflows\n");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { WorkflowExecutor } = await import("../executor.js");
|
|
148
|
+
const { WorkflowParser, WorkflowParseError } = await import("../parser.js");
|
|
149
|
+
|
|
150
|
+
const [workflowPath, locationType] = resolveWorkflowPath(options.workflow);
|
|
151
|
+
|
|
152
|
+
if (!existsSync(workflowPath)) {
|
|
153
|
+
process.stderr.write(`Error: Workflow not found: ${options.workflow}\n`);
|
|
154
|
+
process.stderr.write("\nAvailable workflows:\n");
|
|
155
|
+
|
|
156
|
+
const workflows = listAvailableWorkflows();
|
|
157
|
+
if (workflows.length > 0) {
|
|
158
|
+
for (const [name, , location] of workflows.slice(0, 10)) {
|
|
159
|
+
process.stderr.write(` ${name} (${location})\n`);
|
|
160
|
+
}
|
|
161
|
+
if (workflows.length > 10) {
|
|
162
|
+
process.stderr.write(` ... and ${workflows.length - 10} more\n`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
process.stderr.write(" (no workflows found)\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
process.stderr.write("\nUse 'agentic-forge run --list' to see all workflows.\n");
|
|
169
|
+
process.stderr.write("Use 'agentic-forge init' to copy bundled workflows locally.\n");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Show which workflow is being used
|
|
174
|
+
if (["project-local", "user-global", "bundled"].includes(locationType)) {
|
|
175
|
+
process.stdout.write(`Using ${locationType} workflow: ${path.basename(workflowPath)}\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse variables
|
|
179
|
+
const variables: Record<string, string> = {};
|
|
180
|
+
if (options.vars) {
|
|
181
|
+
for (const v of options.vars) {
|
|
182
|
+
if (!v.includes("=")) {
|
|
183
|
+
process.stderr.write(`Error: Invalid variable format: ${v}\n`);
|
|
184
|
+
process.stderr.write("Expected format: KEY=VALUE\n");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const eqIndex = v.indexOf("=");
|
|
188
|
+
variables[v.slice(0, eqIndex)] = v.slice(eqIndex + 1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let workflow: import("../types.js").WorkflowDefinition;
|
|
193
|
+
try {
|
|
194
|
+
const parser = new WorkflowParser();
|
|
195
|
+
workflow = parser.parseFile(workflowPath);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (e instanceof WorkflowParseError) {
|
|
198
|
+
process.stderr.write(`Error parsing workflow: ${e.message}\n`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const executor = new WorkflowExecutor();
|
|
205
|
+
try {
|
|
206
|
+
// Resolve terminal_output: CLI override > workflow settings > default "base"
|
|
207
|
+
let terminalOutput = "base";
|
|
208
|
+
if (options.terminalOutput != null) {
|
|
209
|
+
terminalOutput = options.terminalOutput;
|
|
210
|
+
} else if (workflow.settings?.terminalOutput) {
|
|
211
|
+
terminalOutput = workflow.settings.terminalOutput;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const progress = await executor.run(
|
|
215
|
+
workflow,
|
|
216
|
+
variables,
|
|
217
|
+
options.fromStep ?? null,
|
|
218
|
+
terminalOutput,
|
|
219
|
+
path.resolve(workflowPath),
|
|
220
|
+
);
|
|
221
|
+
process.stdout.write(`\nWorkflow ${progress.status}: ${progress.workflowId}\n`);
|
|
222
|
+
if (progress.errors && progress.errors.length > 0) {
|
|
223
|
+
process.stdout.write("\nErrors:\n");
|
|
224
|
+
for (const error of progress.errors) {
|
|
225
|
+
process.stdout.write(
|
|
226
|
+
` - ${(error as Record<string, string>).step}: ${(error as Record<string, string>).error}\n`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (e) {
|
|
231
|
+
process.stderr.write(`Error running workflow: ${(e as Error).message}\n`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Shortcut command handlers (input). */
|
|
2
|
+
|
|
3
|
+
import { processHumanInput } from "../orchestrator.js";
|
|
4
|
+
|
|
5
|
+
export function cmdInput(workflowId: string, response: string): void {
|
|
6
|
+
if (processHumanInput(workflowId, response)) {
|
|
7
|
+
process.stdout.write(`Input recorded for workflow: ${workflowId}\n`);
|
|
8
|
+
} else {
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Skills-dir command handler. */
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function cmdSkillsDir(): void {
|
|
9
|
+
const skillsDir = path.resolve(path.join(__dirname, "..", "claude"));
|
|
10
|
+
process.stdout.write(`${skillsDir}\n`);
|
|
11
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Status, cancel, and list command handlers. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { WORKFLOW_STATUS, loadProgress, saveProgress } from "../progress.js";
|
|
7
|
+
|
|
8
|
+
export function cmdStatus(workflowId: string): void {
|
|
9
|
+
const progress = loadProgress(workflowId);
|
|
10
|
+
if (progress === null) {
|
|
11
|
+
process.stderr.write(`Error: Workflow not found: ${workflowId}\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process.stdout.write(`Workflow: ${progress.workflowName}\n`);
|
|
16
|
+
process.stdout.write(`ID: ${progress.workflowId}\n`);
|
|
17
|
+
process.stdout.write(`Status: ${progress.status}\n`);
|
|
18
|
+
process.stdout.write(`Started: ${progress.startedAt}\n`);
|
|
19
|
+
if (progress.completedAt) {
|
|
20
|
+
process.stdout.write(`Completed: ${progress.completedAt}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (progress.currentStep) {
|
|
24
|
+
const step = progress.currentStep as Record<string, unknown>;
|
|
25
|
+
process.stdout.write(`\nCurrent Step: ${step.name}\n`);
|
|
26
|
+
process.stdout.write(` Retry Count: ${(step.retry_count as number) ?? 0}\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (progress.completedSteps && progress.completedSteps.length > 0) {
|
|
30
|
+
process.stdout.write("\nCompleted Steps:\n");
|
|
31
|
+
for (const step of progress.completedSteps) {
|
|
32
|
+
const icon = step.status === "completed" ? "+" : "x";
|
|
33
|
+
process.stdout.write(` [${icon}] ${step.name}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (progress.pendingSteps && progress.pendingSteps.length > 0) {
|
|
38
|
+
process.stdout.write("\nPending Steps:\n");
|
|
39
|
+
for (const stepName of progress.pendingSteps) {
|
|
40
|
+
process.stdout.write(` [ ] ${stepName}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (progress.errors && progress.errors.length > 0) {
|
|
45
|
+
process.stdout.write("\nErrors:\n");
|
|
46
|
+
for (const error of progress.errors) {
|
|
47
|
+
const err = error as Record<string, string>;
|
|
48
|
+
process.stdout.write(` - ${err.step}: ${err.error}\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function cmdCancel(workflowId: string): void {
|
|
54
|
+
const progress = loadProgress(workflowId);
|
|
55
|
+
if (progress === null) {
|
|
56
|
+
process.stderr.write(`Error: Workflow not found: ${workflowId}\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (progress.status !== WORKFLOW_STATUS.RUNNING && progress.status !== WORKFLOW_STATUS.PAUSED) {
|
|
61
|
+
process.stderr.write(`Error: Cannot cancel workflow in '${progress.status}' status\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
progress.status = WORKFLOW_STATUS.CANCELED;
|
|
66
|
+
progress.completedAt = new Date().toISOString();
|
|
67
|
+
saveProgress(progress);
|
|
68
|
+
|
|
69
|
+
process.stdout.write(`Workflow canceled: ${workflowId}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function cmdList(statusFilter?: string): void {
|
|
73
|
+
const outputsDir = path.join(process.cwd(), "agentic", "outputs");
|
|
74
|
+
if (!existsSync(outputsDir)) {
|
|
75
|
+
process.stdout.write("No workflows found.\n");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const workflows: Record<string, unknown>[] = [];
|
|
80
|
+
const entries = readdirSync(outputsDir, { withFileTypes: true });
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
const progressFile = path.join(outputsDir, entry.name, "progress.json");
|
|
84
|
+
if (existsSync(progressFile)) {
|
|
85
|
+
const data = JSON.parse(readFileSync(progressFile, "utf-8")) as Record<string, unknown>;
|
|
86
|
+
if (statusFilter == null || data.status === statusFilter) {
|
|
87
|
+
workflows.push(data);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (workflows.length === 0) {
|
|
94
|
+
const suffix = statusFilter ? ` (status=${statusFilter})` : "";
|
|
95
|
+
process.stdout.write(`No workflows found.${suffix}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.stdout.write(
|
|
100
|
+
`${"ID".padEnd(12)} ${"Name".padEnd(25)} ${"Status".padEnd(12)} ${"Started".padEnd(20)}\n`,
|
|
101
|
+
);
|
|
102
|
+
process.stdout.write(`${"-".repeat(70)}\n`);
|
|
103
|
+
for (const wf of workflows) {
|
|
104
|
+
const id = String(wf.workflow_id ?? "").padEnd(12);
|
|
105
|
+
const name = String(wf.workflow_name ?? "")
|
|
106
|
+
.slice(0, 25)
|
|
107
|
+
.padEnd(25);
|
|
108
|
+
const status = String(wf.status ?? "").padEnd(12);
|
|
109
|
+
const started = wf.started_at ? String(wf.started_at).slice(0, 19).padEnd(20) : "".padEnd(20);
|
|
110
|
+
process.stdout.write(`${id} ${name} ${status} ${started}\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|