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.
Files changed (110) hide show
  1. package/.gitattributes +24 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.markdownlint-cli2.jsonc +16 -0
  4. package/.prettierignore +3 -0
  5. package/.prettierrc +6 -0
  6. package/.vscode/agentic-forge.code-workspace +26 -0
  7. package/CHANGELOG.md +100 -0
  8. package/CLAUDE.md +158 -0
  9. package/CONTRIBUTING.md +152 -0
  10. package/LICENSE +21 -0
  11. package/README.md +145 -0
  12. package/agentic-forge-banner.png +0 -0
  13. package/biome.json +21 -0
  14. package/package.json +5 -0
  15. package/scripts/copy-assets.js +21 -0
  16. package/src/agents/explorer.md +97 -0
  17. package/src/agents/reviewer.md +137 -0
  18. package/src/checkpoints/manager.ts +119 -0
  19. package/src/claude/.claude/skills/analyze/SKILL.md +241 -0
  20. package/src/claude/.claude/skills/analyze/references/bug.md +62 -0
  21. package/src/claude/.claude/skills/analyze/references/debt.md +76 -0
  22. package/src/claude/.claude/skills/analyze/references/doc.md +67 -0
  23. package/src/claude/.claude/skills/analyze/references/security.md +76 -0
  24. package/src/claude/.claude/skills/analyze/references/style.md +72 -0
  25. package/src/claude/.claude/skills/create-checkpoint/SKILL.md +88 -0
  26. package/src/claude/.claude/skills/create-log/SKILL.md +75 -0
  27. package/src/claude/.claude/skills/fix-analyze/SKILL.md +102 -0
  28. package/src/claude/.claude/skills/git-branch/SKILL.md +71 -0
  29. package/src/claude/.claude/skills/git-commit/SKILL.md +107 -0
  30. package/src/claude/.claude/skills/git-pr/SKILL.md +96 -0
  31. package/src/claude/.claude/skills/orchestrate/SKILL.md +120 -0
  32. package/src/claude/.claude/skills/sdlc-plan/SKILL.md +163 -0
  33. package/src/claude/.claude/skills/sdlc-plan/references/bug.md +115 -0
  34. package/src/claude/.claude/skills/sdlc-plan/references/chore.md +105 -0
  35. package/src/claude/.claude/skills/sdlc-plan/references/feature.md +130 -0
  36. package/src/claude/.claude/skills/sdlc-review/SKILL.md +215 -0
  37. package/src/claude/.claude/skills/workflow-builder/SKILL.md +185 -0
  38. package/src/claude/.claude/skills/workflow-builder/references/REFERENCE.md +487 -0
  39. package/src/claude/.claude/skills/workflow-builder/references/workflow-example.yaml +427 -0
  40. package/src/cli.ts +182 -0
  41. package/src/commands/config-cmd.ts +28 -0
  42. package/src/commands/index.ts +21 -0
  43. package/src/commands/init.ts +96 -0
  44. package/src/commands/release-notes.ts +85 -0
  45. package/src/commands/resume.ts +103 -0
  46. package/src/commands/run.ts +234 -0
  47. package/src/commands/shortcuts.ts +11 -0
  48. package/src/commands/skills-dir.ts +11 -0
  49. package/src/commands/status.ts +112 -0
  50. package/src/commands/update.ts +64 -0
  51. package/src/commands/version.ts +27 -0
  52. package/src/commands/workflows.ts +129 -0
  53. package/src/config.ts +129 -0
  54. package/src/console.ts +790 -0
  55. package/src/executor.ts +354 -0
  56. package/src/git/worktree.ts +236 -0
  57. package/src/logging/logger.ts +95 -0
  58. package/src/orchestrator.ts +815 -0
  59. package/src/parser.ts +225 -0
  60. package/src/progress.ts +306 -0
  61. package/src/prompts/agentic-system.md +31 -0
  62. package/src/ralph-loop.ts +260 -0
  63. package/src/renderer.ts +164 -0
  64. package/src/runner.ts +634 -0
  65. package/src/signal-manager.ts +55 -0
  66. package/src/steps/base.ts +71 -0
  67. package/src/steps/conditional-step.ts +144 -0
  68. package/src/steps/index.ts +15 -0
  69. package/src/steps/parallel-step.ts +213 -0
  70. package/src/steps/prompt-step.ts +121 -0
  71. package/src/steps/ralph-loop-step.ts +186 -0
  72. package/src/steps/serial-step.ts +84 -0
  73. package/src/templates/analysis/bug.md.j2 +35 -0
  74. package/src/templates/analysis/debt.md.j2 +38 -0
  75. package/src/templates/analysis/doc.md.j2 +45 -0
  76. package/src/templates/analysis/security.md.j2 +35 -0
  77. package/src/templates/analysis/style.md.j2 +44 -0
  78. package/src/templates/analysis-summary.md.j2 +58 -0
  79. package/src/templates/checkpoint.md.j2 +27 -0
  80. package/src/templates/implementation-report.md.j2 +81 -0
  81. package/src/templates/memory.md.j2 +16 -0
  82. package/src/templates/plan-bug.md.j2 +42 -0
  83. package/src/templates/plan-chore.md.j2 +27 -0
  84. package/src/templates/plan-feature.md.j2 +41 -0
  85. package/src/templates/progress.json.j2 +16 -0
  86. package/src/templates/ralph-report.md.j2 +45 -0
  87. package/src/types.ts +141 -0
  88. package/src/workflows/analyze-codebase-merge.yaml +328 -0
  89. package/src/workflows/analyze-codebase.yaml +196 -0
  90. package/src/workflows/analyze-single.yaml +56 -0
  91. package/src/workflows/demo.yaml +180 -0
  92. package/src/workflows/one-shot.yaml +54 -0
  93. package/src/workflows/plan-build-review.yaml +160 -0
  94. package/src/workflows/ralph-loop.yaml +73 -0
  95. package/tests/config.test.ts +219 -0
  96. package/tests/console.test.ts +506 -0
  97. package/tests/executor.test.ts +339 -0
  98. package/tests/init.test.ts +86 -0
  99. package/tests/logger.test.ts +110 -0
  100. package/tests/parser.test.ts +290 -0
  101. package/tests/progress.test.ts +345 -0
  102. package/tests/ralph-loop.test.ts +418 -0
  103. package/tests/renderer.test.ts +350 -0
  104. package/tests/runner.test.ts +497 -0
  105. package/tests/setup.test.ts +7 -0
  106. package/tests/signal-manager.test.ts +26 -0
  107. package/tests/steps.test.ts +412 -0
  108. package/tests/worktree.test.ts +411 -0
  109. package/tsconfig.json +18 -0
  110. package/vitest.config.ts +8 -0
@@ -0,0 +1,354 @@
1
+ /** Workflow executor for running workflows. */
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { loadConfig } from "./config.js";
7
+ import type { ConsoleOutput } from "./console.js";
8
+ import { OutputLevel } from "./console.js";
9
+ import { ConsoleOutput as ConsoleOutputClass } from "./console.js";
10
+ import type { WorkflowLogger } from "./logging/logger.js";
11
+ import { WorkflowLogger as WorkflowLoggerClass } from "./logging/logger.js";
12
+ import {
13
+ STEP_STATUS,
14
+ WORKFLOW_STATUS,
15
+ createProgress,
16
+ generateWorkflowId,
17
+ saveProgress,
18
+ updateStepFailed,
19
+ updateStepStarted,
20
+ } from "./progress.js";
21
+ import { TemplateRenderer, buildTemplateContext, renderWorkflowOutput } from "./renderer.js";
22
+ import { type StepContext, type StepResult, resolveModel } from "./steps/base.js";
23
+ import { ConditionalStepExecutor } from "./steps/conditional-step.js";
24
+ import { ParallelStepExecutor } from "./steps/parallel-step.js";
25
+ import { PromptStepExecutor } from "./steps/prompt-step.js";
26
+ import { RalphLoopStepExecutor } from "./steps/ralph-loop-step.js";
27
+ import { SerialStepExecutor } from "./steps/serial-step.js";
28
+ import type {
29
+ OutputDefinition,
30
+ StepDefinition,
31
+ StepType,
32
+ WorkflowDefinition,
33
+ WorkflowProgress,
34
+ WorkflowSettings,
35
+ } from "./types.js";
36
+
37
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
+
39
+ export class WorkflowExecutor {
40
+ repoRoot: string;
41
+ config: Record<string, unknown>;
42
+ strictMode: boolean;
43
+ renderer: TemplateRenderer;
44
+ workflowSettings: WorkflowSettings | null = null;
45
+ executors: Record<
46
+ string,
47
+ | PromptStepExecutor
48
+ | ParallelStepExecutor
49
+ | SerialStepExecutor
50
+ | ConditionalStepExecutor
51
+ | RalphLoopStepExecutor
52
+ >;
53
+
54
+ promptExecutor: PromptStepExecutor;
55
+ ralphLoopExecutor: RalphLoopStepExecutor;
56
+ parallelExecutor: ParallelStepExecutor;
57
+ serialExecutor: SerialStepExecutor;
58
+ conditionalExecutor: ConditionalStepExecutor;
59
+
60
+ constructor(repoRoot?: string, strictMode = false) {
61
+ this.repoRoot = repoRoot ?? process.cwd();
62
+ this.config = loadConfig(this.repoRoot);
63
+ this.strictMode = strictMode;
64
+ this.renderer = new TemplateRenderer(undefined, strictMode);
65
+
66
+ // Initialize step executors
67
+ this.promptExecutor = new PromptStepExecutor();
68
+ this.ralphLoopExecutor = new RalphLoopStepExecutor();
69
+
70
+ // Branch executor function for nested steps
71
+ const branchExecutors: Record<
72
+ string,
73
+ {
74
+ execute: (
75
+ step: StepDefinition,
76
+ progress: WorkflowProgress,
77
+ context: StepContext,
78
+ logger: WorkflowLogger,
79
+ console: ConsoleOutput,
80
+ ) => Promise<StepResult>;
81
+ }
82
+ > = {
83
+ prompt: this.promptExecutor,
84
+ "ralph-loop": this.ralphLoopExecutor,
85
+ };
86
+
87
+ const branchExecute = async (
88
+ step: StepDefinition,
89
+ progress: WorkflowProgress,
90
+ context: StepContext,
91
+ logger: WorkflowLogger,
92
+ console: ConsoleOutput,
93
+ ): Promise<StepResult> => {
94
+ const executor = branchExecutors[step.type] ?? this.executors[step.type];
95
+ if (!executor) {
96
+ throw new Error(`Step type not yet implemented: ${step.type}`);
97
+ }
98
+ return executor.execute(step, progress, context, logger, console);
99
+ };
100
+
101
+ this.parallelExecutor = new ParallelStepExecutor(branchExecute);
102
+ this.serialExecutor = new SerialStepExecutor(branchExecute);
103
+ this.conditionalExecutor = new ConditionalStepExecutor(branchExecute);
104
+
105
+ // Add complex executors to branch lookup
106
+ branchExecutors.serial = this.serialExecutor;
107
+ branchExecutors.conditional = this.conditionalExecutor;
108
+
109
+ this.executors = {
110
+ prompt: this.promptExecutor,
111
+ parallel: this.parallelExecutor,
112
+ serial: this.serialExecutor,
113
+ conditional: this.conditionalExecutor,
114
+ "ralph-loop": this.ralphLoopExecutor,
115
+ "wait-for-human": {
116
+ execute: async (
117
+ step: StepDefinition,
118
+ progress: WorkflowProgress,
119
+ _context: StepContext,
120
+ logger: WorkflowLogger,
121
+ ): Promise<StepResult> => {
122
+ logger.info(step.name, `Waiting for human input: ${step.message}`);
123
+ progress.status = WORKFLOW_STATUS.PAUSED;
124
+ progress.currentStep = {
125
+ name: step.name,
126
+ type: "wait-for-human",
127
+ message: step.message,
128
+ started_at: new Date().toISOString(),
129
+ timeout_minutes: step.stepTimeoutMinutes ?? 5,
130
+ on_timeout: step.onTimeout,
131
+ };
132
+ return { success: true, outputSummary: "Paused for human input" };
133
+ },
134
+ },
135
+ };
136
+ }
137
+
138
+ async run(
139
+ workflow: WorkflowDefinition,
140
+ variables?: Record<string, unknown>,
141
+ fromStep?: string | null,
142
+ terminalOutput = "base",
143
+ workflowFile = "",
144
+ resumeProgress?: WorkflowProgress | null,
145
+ ): Promise<WorkflowProgress> {
146
+ let vars = variables ? { ...variables } : {};
147
+ this.workflowSettings = workflow.settings;
148
+
149
+ // Update renderer with workflow's strict_mode setting
150
+ this.renderer = new TemplateRenderer(undefined, workflow.settings.strictMode);
151
+
152
+ // Create console output handler
153
+ const outputLevel = terminalOutput === "all" ? OutputLevel.ALL : OutputLevel.BASE;
154
+ const console = new ConsoleOutputClass(outputLevel);
155
+
156
+ let progress: WorkflowProgress;
157
+ let workflowId: string;
158
+
159
+ if (resumeProgress) {
160
+ progress = resumeProgress;
161
+ // Seed variables from stored progress, then overlay any new CLI vars
162
+ const merged = { ...progress.variables };
163
+ Object.assign(merged, vars);
164
+ vars = merged;
165
+ progress.variables = vars;
166
+ workflowId = progress.workflowId;
167
+ if (!progress.workflowFile && workflowFile) {
168
+ progress.workflowFile = workflowFile;
169
+ }
170
+ } else {
171
+ for (const v of workflow.variables) {
172
+ if (!(v.name in vars)) {
173
+ if (v.required && v.default === undefined) {
174
+ throw new Error(`Missing required variable: ${v.name}`);
175
+ }
176
+ vars[v.name] = v.default;
177
+ }
178
+ }
179
+
180
+ workflowId = generateWorkflowId(workflow.name);
181
+ const stepNames = workflow.steps.map((s) => s.name);
182
+ progress = createProgress(workflowId, workflow.name, stepNames, vars, workflowFile);
183
+ }
184
+
185
+ saveProgress(progress, this.repoRoot);
186
+
187
+ const logger = new WorkflowLoggerClass(workflowId, this.repoRoot);
188
+ logger.info("workflow", `Started workflow: ${workflow.name}`);
189
+
190
+ console.workflowStart(workflow.name, workflowId);
191
+
192
+ // Build set of completed step names to skip on resume
193
+ const completedStepNames = new Set(
194
+ progress.completedSteps
195
+ .filter((s) => s.status === STEP_STATUS.COMPLETED || s.status === STEP_STATUS.SKIPPED)
196
+ .map((s) => s.name),
197
+ );
198
+
199
+ let skipUntil = fromStep ?? null;
200
+
201
+ for (const step of workflow.steps) {
202
+ if (skipUntil) {
203
+ if (step.name === skipUntil) {
204
+ skipUntil = null;
205
+ } else {
206
+ continue;
207
+ }
208
+ }
209
+
210
+ // Skip steps already completed/skipped during resume
211
+ if (completedStepNames.has(step.name)) {
212
+ logger.info(step.name, `Skipping already completed step: ${step.name}`);
213
+ console.info(`Skipping completed step: ${step.name}`);
214
+ continue;
215
+ }
216
+
217
+ try {
218
+ await this.executeStep(step, progress, vars, logger, console);
219
+ saveProgress(progress, this.repoRoot);
220
+
221
+ if (progress.status === WORKFLOW_STATUS.FAILED) {
222
+ break;
223
+ }
224
+ } catch (e: unknown) {
225
+ const errorMsg = e instanceof Error ? e.message : String(e);
226
+ logger.error(step.name, `Step failed: ${errorMsg}`);
227
+ console.stepFailed(step.name, errorMsg);
228
+ updateStepFailed(progress, step.name, errorMsg);
229
+ progress.status = WORKFLOW_STATUS.FAILED;
230
+ saveProgress(progress, this.repoRoot);
231
+ break;
232
+ }
233
+ }
234
+
235
+ if (progress.status === WORKFLOW_STATUS.RUNNING) {
236
+ progress.status = WORKFLOW_STATUS.COMPLETED;
237
+ }
238
+ progress.completedAt = new Date().toISOString();
239
+ saveProgress(progress, this.repoRoot);
240
+
241
+ this.renderOutputs(workflow, progress, vars, logger);
242
+
243
+ console.workflowComplete(workflow.name, progress.status);
244
+ logger.info("workflow", `Workflow ${progress.status}: ${workflow.name}`);
245
+ return progress;
246
+ }
247
+
248
+ private renderOutputs(
249
+ workflow: WorkflowDefinition,
250
+ progress: WorkflowProgress,
251
+ variables: Record<string, unknown>,
252
+ logger: WorkflowLogger,
253
+ ): void {
254
+ if (!workflow.outputs || workflow.outputs.length === 0) {
255
+ return;
256
+ }
257
+
258
+ const stepOutputs: Record<string, Record<string, unknown>> = {};
259
+ for (const step of progress.completedSteps) {
260
+ const entry: Record<string, unknown> = {
261
+ status: step.status,
262
+ started_at: step.startedAt,
263
+ completed_at: step.completedAt,
264
+ output_summary: step.outputSummary,
265
+ error: step.error,
266
+ };
267
+ if (step.name in progress.stepOutputs) {
268
+ entry.output = progress.stepOutputs[step.name];
269
+ }
270
+ stepOutputs[step.name] = entry;
271
+ }
272
+
273
+ const context = buildTemplateContext(
274
+ workflow.name,
275
+ progress.workflowId,
276
+ progress.startedAt ?? "",
277
+ progress.completedAt,
278
+ stepOutputs,
279
+ [],
280
+ [],
281
+ [],
282
+ variables,
283
+ );
284
+
285
+ const pluginTemplates = path.join(__dirname, "templates");
286
+ const templateDirs = [pluginTemplates, this.repoRoot];
287
+
288
+ const outputDir = path.join(this.repoRoot, "agentic", "outputs", progress.workflowId);
289
+
290
+ for (const output of workflow.outputs) {
291
+ if (output.when === "completed" && progress.status !== WORKFLOW_STATUS.COMPLETED) {
292
+ continue;
293
+ }
294
+ if (output.when === "failed" && progress.status !== WORKFLOW_STATUS.FAILED) {
295
+ continue;
296
+ }
297
+
298
+ try {
299
+ let outputPathStr = output.path;
300
+ if (this.renderer.hasVariables(outputPathStr)) {
301
+ outputPathStr = this.renderer.renderString(outputPathStr, {
302
+ workflow_id: progress.workflowId,
303
+ ...variables,
304
+ });
305
+ }
306
+ const outputPath = path.join(outputDir, outputPathStr);
307
+
308
+ renderWorkflowOutput(
309
+ output.template,
310
+ outputPath,
311
+ context,
312
+ templateDirs,
313
+ workflow.settings.strictMode,
314
+ );
315
+ logger.info("workflow", `Generated output: ${outputPath}`);
316
+ } catch (e: unknown) {
317
+ const errorMsg = e instanceof Error ? e.message : String(e);
318
+ logger.error("workflow", `Failed to render output '${output.name}': ${errorMsg}`);
319
+ }
320
+ }
321
+ }
322
+
323
+ async executeStep(
324
+ step: StepDefinition,
325
+ progress: WorkflowProgress,
326
+ variables: Record<string, unknown>,
327
+ logger: WorkflowLogger,
328
+ console: ConsoleOutput,
329
+ ): Promise<void> {
330
+ const context: StepContext = {
331
+ repoRoot: this.repoRoot,
332
+ config: this.config,
333
+ renderer: this.renderer,
334
+ workflowSettings: this.workflowSettings,
335
+ workflowId: progress.workflowId,
336
+ variables,
337
+ outputs: progress.stepOutputs,
338
+ };
339
+
340
+ const resolvedModel = resolveModel(context, step.model);
341
+
342
+ logger.info(step.name, `Starting step: ${step.name}`);
343
+ console.stepStart(step.name, step.type, resolvedModel);
344
+ updateStepStarted(progress, step.name);
345
+ saveProgress(progress, this.repoRoot);
346
+
347
+ const executor = this.executors[step.type];
348
+ if (!executor) {
349
+ throw new Error(`Step type not yet implemented: ${step.type}`);
350
+ }
351
+
352
+ await executor.execute(step, progress, context, logger, console);
353
+ }
354
+ }
@@ -0,0 +1,236 @@
1
+ /** Git worktree management for parallel workflow execution. */
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { randomBytes } from "node:crypto";
5
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { getExecutable } from "../runner.js";
8
+
9
+ // --- Worktree data ---
10
+
11
+ export interface Worktree {
12
+ path: string;
13
+ branch: string;
14
+ baseBranch: string;
15
+ }
16
+
17
+ // --- Git command helper ---
18
+
19
+ export function runGit(
20
+ args: string[],
21
+ cwd?: string | null,
22
+ check = true,
23
+ ): { stdout: string; stderr: string; returncode: number } {
24
+ const gitPath = getExecutable("git");
25
+ const cmd = [gitPath, ...args];
26
+ try {
27
+ const stdout = execFileSync(gitPath, args, {
28
+ encoding: "utf-8",
29
+ cwd: cwd ?? undefined,
30
+ stdio: ["pipe", "pipe", "pipe"],
31
+ });
32
+ return { stdout, stderr: "", returncode: 0 };
33
+ } catch (e: unknown) {
34
+ const err = e as { stdout?: string; stderr?: string; status?: number };
35
+ if (check) {
36
+ throw new Error(`Git command failed: ${cmd.join(" ")}\n${err.stderr ?? ""}`);
37
+ }
38
+ return {
39
+ stdout: (err.stdout as string) ?? "",
40
+ stderr: (err.stderr as string) ?? "",
41
+ returncode: (err.status as number) ?? 1,
42
+ };
43
+ }
44
+ }
45
+
46
+ // --- Name helpers ---
47
+
48
+ function truncate(name: string, maxLen = 30): string {
49
+ return name.length > maxLen ? name.slice(0, maxLen) : name;
50
+ }
51
+
52
+ function generateSuffix(): string {
53
+ return randomBytes(3).toString("hex");
54
+ }
55
+
56
+ function sanitizeName(name: string): string {
57
+ return name.replace(/\//g, "-").replace(/ /g, "-").replace(/_/g, "-").toLowerCase();
58
+ }
59
+
60
+ // --- Repository helpers ---
61
+
62
+ export function getRepoRoot(cwd?: string | null): string {
63
+ const result = runGit(["rev-parse", "--show-toplevel"], cwd);
64
+ return result.stdout.trim();
65
+ }
66
+
67
+ export function getDefaultBranch(cwd?: string | null): string {
68
+ const result = runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd, false);
69
+ if (result.returncode === 0) {
70
+ const parts = result.stdout.trim().split("/");
71
+ return parts[parts.length - 1] ?? "main";
72
+ }
73
+ for (const branch of ["main", "master"]) {
74
+ const check = runGit(["rev-parse", "--verify", branch], cwd, false);
75
+ if (check.returncode === 0) {
76
+ return branch;
77
+ }
78
+ }
79
+ return "main";
80
+ }
81
+
82
+ export function getCurrentBranch(cwd?: string | null): string {
83
+ const result = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
84
+ return result.stdout.trim();
85
+ }
86
+
87
+ // --- Worktree operations ---
88
+
89
+ export function createWorktree(
90
+ workflowName: string,
91
+ stepName: string,
92
+ baseBranch?: string | null,
93
+ repoRoot?: string | null,
94
+ ): Worktree {
95
+ const root = repoRoot || getRepoRoot();
96
+ const base = baseBranch || getDefaultBranch(root);
97
+
98
+ const suffix = generateSuffix();
99
+ const wfName = truncate(sanitizeName(workflowName));
100
+ const stName = truncate(sanitizeName(stepName));
101
+
102
+ const dirName = `agentic-${wfName}-${stName}-${suffix}`;
103
+ const branchName = `agentic/${wfName}-${stName}-${suffix}`;
104
+
105
+ const worktreePath = path.join(root, ".worktrees", dirName);
106
+ const parentDir = path.dirname(worktreePath);
107
+ mkdirSync(parentDir, { recursive: true });
108
+
109
+ if (existsSync(worktreePath)) {
110
+ rmSync(worktreePath, { recursive: true, force: true });
111
+ runGit(["worktree", "prune"], root, false);
112
+ }
113
+
114
+ runGit(["worktree", "add", "-b", branchName, worktreePath, base], root);
115
+
116
+ return { path: worktreePath, branch: branchName, baseBranch: base };
117
+ }
118
+
119
+ export function removeWorktree(
120
+ worktree: Worktree,
121
+ repoRoot?: string | null,
122
+ deleteBranch = true,
123
+ ): void {
124
+ const root = repoRoot || getRepoRoot();
125
+
126
+ const result = runGit(["worktree", "remove", "--force", worktree.path], root, false);
127
+
128
+ if (result.returncode !== 0 && existsSync(worktree.path)) {
129
+ rmSync(worktree.path, { recursive: true, force: true });
130
+ runGit(["worktree", "prune"], root, false);
131
+ }
132
+
133
+ if (deleteBranch && worktree.branch) {
134
+ runGit(["branch", "-D", worktree.branch], root, false);
135
+ }
136
+ }
137
+
138
+ export function listWorktrees(repoRoot?: string | null): Worktree[] {
139
+ const root = repoRoot || getRepoRoot();
140
+
141
+ const result = runGit(["worktree", "list", "--porcelain"], root);
142
+
143
+ const worktrees: Worktree[] = [];
144
+ let currentPath: string | null = null;
145
+ let currentBranch = "";
146
+
147
+ for (const line of result.stdout.trim().split("\n")) {
148
+ if (line.startsWith("worktree ")) {
149
+ currentPath = line.slice(9);
150
+ } else if (line.startsWith("branch ")) {
151
+ currentBranch = line.replace("branch refs/heads/", "");
152
+ } else if (line === "" && currentPath) {
153
+ worktrees.push({ path: currentPath, branch: currentBranch, baseBranch: "" });
154
+ currentPath = null;
155
+ currentBranch = "";
156
+ }
157
+ }
158
+
159
+ if (currentPath) {
160
+ worktrees.push({ path: currentPath, branch: currentBranch, baseBranch: "" });
161
+ }
162
+
163
+ return worktrees;
164
+ }
165
+
166
+ export function listAgenticWorktrees(repoRoot?: string | null): Worktree[] {
167
+ return listWorktrees(repoRoot).filter((wt) => wt.branch.startsWith("agentic/"));
168
+ }
169
+
170
+ export function pruneOrphaned(repoRoot?: string | null): number {
171
+ const root = repoRoot || getRepoRoot();
172
+
173
+ runGit(["worktree", "prune"], root, false);
174
+
175
+ let cleaned = 0;
176
+ const worktreesDir = path.join(root, ".worktrees");
177
+ if (existsSync(worktreesDir)) {
178
+ for (const entry of readdirSync(worktreesDir)) {
179
+ const wtDir = path.join(worktreesDir, entry);
180
+ if (statSync(wtDir).isDirectory() && entry.startsWith("agentic-")) {
181
+ const gitFile = path.join(wtDir, ".git");
182
+ if (!existsSync(gitFile)) {
183
+ rmSync(wtDir, { recursive: true, force: true });
184
+ cleaned++;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ return cleaned;
191
+ }
192
+
193
+ // --- Branch operations ---
194
+
195
+ export function createBranch(
196
+ branchName: string,
197
+ baseBranch?: string | null,
198
+ cwd?: string | null,
199
+ ): string {
200
+ const base = baseBranch || getDefaultBranch(cwd);
201
+ runGit(["checkout", "-b", branchName, base], cwd);
202
+ return branchName;
203
+ }
204
+
205
+ export function checkoutBranch(branchName: string, cwd?: string | null): void {
206
+ runGit(["checkout", branchName], cwd);
207
+ }
208
+
209
+ export function commitChanges(message: string, cwd?: string | null, addAll = true): boolean {
210
+ if (addAll) {
211
+ runGit(["add", "-A"], cwd);
212
+ }
213
+
214
+ const result = runGit(["status", "--porcelain"], cwd);
215
+ if (!result.stdout.trim()) {
216
+ return false;
217
+ }
218
+
219
+ runGit(["commit", "-m", message], cwd);
220
+ return true;
221
+ }
222
+
223
+ export function pushBranch(
224
+ branchName: string,
225
+ remote = "origin",
226
+ cwd?: string | null,
227
+ setUpstream = true,
228
+ ): void {
229
+ const args = ["push"];
230
+ if (setUpstream) {
231
+ args.push("-u", remote, branchName);
232
+ } else {
233
+ args.push(remote, branchName);
234
+ }
235
+ runGit(args, cwd);
236
+ }
@@ -0,0 +1,95 @@
1
+ /** NDJSON structured logging for workflows. */
2
+
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ // --- Log levels ---
7
+
8
+ export const LOG_LEVEL = {
9
+ CRITICAL: "Critical",
10
+ ERROR: "Error",
11
+ WARNING: "Warning",
12
+ INFORMATION: "Information",
13
+ } as const;
14
+
15
+ export type LogLevel = (typeof LOG_LEVEL)[keyof typeof LOG_LEVEL];
16
+
17
+ // --- Log entry ---
18
+
19
+ export interface LogEntry {
20
+ timestamp: string;
21
+ level: string;
22
+ step: string;
23
+ message: string;
24
+ context: Record<string, unknown> | null;
25
+ }
26
+
27
+ // --- Logger class ---
28
+
29
+ export class WorkflowLogger {
30
+ readonly workflowId: string;
31
+ readonly logPath: string;
32
+
33
+ constructor(workflowId: string, repoRoot?: string) {
34
+ this.workflowId = workflowId;
35
+ const root = repoRoot ?? process.cwd();
36
+ this.logPath = path.join(root, "agentic", "outputs", workflowId, "logs.ndjson");
37
+ mkdirSync(path.dirname(this.logPath), { recursive: true });
38
+ }
39
+
40
+ log(
41
+ level: LogLevel,
42
+ step: string,
43
+ message: string,
44
+ context?: Record<string, unknown> | null,
45
+ ): void {
46
+ const entry: LogEntry = {
47
+ timestamp: new Date().toISOString(),
48
+ level,
49
+ step,
50
+ message,
51
+ context: context ?? null,
52
+ };
53
+ appendFileSync(this.logPath, `${JSON.stringify(entry)}\n`, "utf-8");
54
+ }
55
+
56
+ critical(step: string, message: string, context?: Record<string, unknown>): void {
57
+ this.log(LOG_LEVEL.CRITICAL, step, message, context || null);
58
+ }
59
+
60
+ error(step: string, message: string, context?: Record<string, unknown>): void {
61
+ this.log(LOG_LEVEL.ERROR, step, message, context || null);
62
+ }
63
+
64
+ warning(step: string, message: string, context?: Record<string, unknown>): void {
65
+ this.log(LOG_LEVEL.WARNING, step, message, context || null);
66
+ }
67
+
68
+ info(step: string, message: string, context?: Record<string, unknown>): void {
69
+ this.log(LOG_LEVEL.INFORMATION, step, message, context || null);
70
+ }
71
+ }
72
+
73
+ // --- Utility functions ---
74
+
75
+ export function readLogs(workflowId: string, repoRoot?: string): LogEntry[] {
76
+ const logPath = getLogPath(workflowId, repoRoot);
77
+ if (!existsSync(logPath)) {
78
+ return [];
79
+ }
80
+
81
+ const content = readFileSync(logPath, "utf-8");
82
+ const entries: LogEntry[] = [];
83
+ for (const line of content.split("\n")) {
84
+ const trimmed = line.trim();
85
+ if (trimmed) {
86
+ entries.push(JSON.parse(trimmed) as LogEntry);
87
+ }
88
+ }
89
+ return entries;
90
+ }
91
+
92
+ export function getLogPath(workflowId: string, repoRoot?: string): string {
93
+ const root = repoRoot ?? process.cwd();
94
+ return path.join(root, "agentic", "outputs", workflowId, "logs.ndjson");
95
+ }