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,350 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import {
6
+ TemplateRenderer,
7
+ buildTemplateContext,
8
+ extractAnalysisSteps,
9
+ extractFixSteps,
10
+ renderWorkflowOutput,
11
+ } from "../src/renderer.js";
12
+
13
+ let tempDir: string;
14
+
15
+ beforeEach(() => {
16
+ tempDir = path.join(
17
+ tmpdir(),
18
+ `renderer-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
19
+ );
20
+ mkdirSync(tempDir, { recursive: true });
21
+ });
22
+
23
+ afterEach(() => {
24
+ // Best-effort cleanup
25
+ try {
26
+ const { rmSync } = require("node:fs");
27
+ rmSync(tempDir, { recursive: true, force: true });
28
+ } catch {
29
+ // Ignore cleanup errors on Windows
30
+ }
31
+ });
32
+
33
+ describe("TemplateRenderer", () => {
34
+ it("initializes with default template directory", () => {
35
+ const renderer = new TemplateRenderer();
36
+ expect(renderer.templateDirs.length).toBe(1);
37
+ expect(renderer.templateDirs[0]).toContain("templates");
38
+ });
39
+
40
+ it("initializes with custom template directories", () => {
41
+ const templatesDir = path.join(tempDir, "templates");
42
+ mkdirSync(templatesDir, { recursive: true });
43
+
44
+ const renderer = new TemplateRenderer([templatesDir]);
45
+ expect(renderer.templateDirs).toContain(templatesDir);
46
+ });
47
+
48
+ it("renders basic template string", () => {
49
+ const renderer = new TemplateRenderer();
50
+ const result = renderer.renderString("Hello, {{ name }}!", { name: "World" });
51
+ expect(result).toBe("Hello, World!");
52
+ });
53
+
54
+ it("renders template with loop", () => {
55
+ const renderer = new TemplateRenderer();
56
+ const template = "{% for item in items %}{{ item }}{% endfor %}";
57
+ const result = renderer.renderString(template, { items: ["a", "b", "c"] });
58
+ expect(result).toBe("abc");
59
+ });
60
+
61
+ it("renders template with conditionals", () => {
62
+ const renderer = new TemplateRenderer();
63
+ const template = "{% if enabled %}ON{% else %}OFF{% endif %}";
64
+
65
+ expect(renderer.renderString(template, { enabled: true })).toBe("ON");
66
+ expect(renderer.renderString(template, { enabled: false })).toBe("OFF");
67
+ });
68
+
69
+ it("handles undefined variables in lenient mode (default)", () => {
70
+ const renderer = new TemplateRenderer();
71
+ // In nunjucks lenient mode, undefined variables render as empty string
72
+ const result = renderer.renderString("{{ undefined_var }}", {});
73
+ expect(result).toBe("");
74
+ });
75
+
76
+ it("raises error for undefined variables in strict mode", () => {
77
+ const renderer = new TemplateRenderer(undefined, true);
78
+ expect(() => renderer.renderString("{{ undefined_var }}", {})).toThrow();
79
+ });
80
+
81
+ it("handles undefined vars in loops (returns empty)", () => {
82
+ const renderer = new TemplateRenderer();
83
+ const result = renderer.renderString("{% for x in undefined_list %}{{ x }}{% endfor %}", {});
84
+ expect(result).toBe("");
85
+ });
86
+
87
+ it("renders nested variable access", () => {
88
+ const renderer = new TemplateRenderer();
89
+ const template = "{{ user.name }} - {{ user.email }}";
90
+ const context = { user: { name: "Alice", email: "alice@example.com" } };
91
+ const result = renderer.renderString(template, context);
92
+ expect(result).toBe("Alice - alice@example.com");
93
+ });
94
+
95
+ it("renders from template file", () => {
96
+ const templatesDir = path.join(tempDir, "templates");
97
+ mkdirSync(templatesDir, { recursive: true });
98
+ writeFileSync(path.join(templatesDir, "test.j2"), "Name: {{ name }}");
99
+
100
+ const renderer = new TemplateRenderer([templatesDir]);
101
+ const result = renderer.render("test.j2", { name: "Test" });
102
+ expect(result).toBe("Name: Test");
103
+ });
104
+
105
+ it("has_variables returns true for templates", () => {
106
+ const renderer = new TemplateRenderer();
107
+ expect(renderer.hasVariables("{{ variable }}")).toBe(true);
108
+ expect(renderer.hasVariables("{% if x %}{% endif %}")).toBe(true);
109
+ });
110
+
111
+ it("has_variables returns false for plain text", () => {
112
+ const renderer = new TemplateRenderer();
113
+ expect(renderer.hasVariables("plain text")).toBe(false);
114
+ expect(renderer.hasVariables("no templates here")).toBe(false);
115
+ });
116
+
117
+ it("adds template directory", () => {
118
+ const newDir = path.join(tempDir, "new_templates");
119
+ mkdirSync(newDir, { recursive: true });
120
+
121
+ const renderer = new TemplateRenderer();
122
+ const originalCount = renderer.templateDirs.length;
123
+
124
+ renderer.addTemplateDir(newDir);
125
+
126
+ expect(renderer.templateDirs.length).toBe(originalCount + 1);
127
+ expect(renderer.templateDirs).toContain(newDir);
128
+ });
129
+
130
+ it("ignores nonexistent template directory", () => {
131
+ const nonexistent = path.join(tempDir, "nonexistent");
132
+ const renderer = new TemplateRenderer();
133
+ const originalCount = renderer.templateDirs.length;
134
+
135
+ renderer.addTemplateDir(nonexistent);
136
+
137
+ expect(renderer.templateDirs.length).toBe(originalCount);
138
+ });
139
+
140
+ it("ignores duplicate template directory", () => {
141
+ const newDir = path.join(tempDir, "templates");
142
+ mkdirSync(newDir, { recursive: true });
143
+
144
+ const renderer = new TemplateRenderer([newDir]);
145
+ renderer.addTemplateDir(newDir);
146
+
147
+ expect(renderer.templateDirs.filter((d) => d === newDir).length).toBe(1);
148
+ });
149
+ });
150
+
151
+ describe("sandboxed environment", () => {
152
+ it("allows normal operations", () => {
153
+ const renderer = new TemplateRenderer();
154
+
155
+ const result = renderer.renderString("{{ name | upper }}", { name: "test" });
156
+ expect(result).toBe("TEST");
157
+
158
+ const result2 = renderer.renderString("{{ items | length }}", { items: [1, 2, 3] });
159
+ expect(result2).toBe("3");
160
+ });
161
+
162
+ it("allows standard filters", () => {
163
+ const renderer = new TemplateRenderer();
164
+
165
+ expect(renderer.renderString("{{ x | default('N/A') }}", {})).toBe("N/A");
166
+ expect(renderer.renderString("{{ 'test' | capitalize }}", {})).toBe("Test");
167
+ expect(renderer.renderString("{{ [1,2,3] | join('-') }}", {})).toBe("1-2-3");
168
+ });
169
+ });
170
+
171
+ describe("extractAnalysisSteps", () => {
172
+ it("extracts analysis steps from outputs", () => {
173
+ const stepOutputs = {
174
+ "analyze-bug": { issues: 5 },
175
+ "analyze-debt": { issues: 3 },
176
+ "analyze-security": { issues: 2 },
177
+ "other-step": { data: "ignored" },
178
+ };
179
+
180
+ const result = extractAnalysisSteps(stepOutputs);
181
+
182
+ expect(result["analyze-bug"]).toBeDefined();
183
+ expect(result["analyze-debt"]).toBeDefined();
184
+ expect(result["analyze-security"]).toBeDefined();
185
+ expect(result["other-step"]).toBeUndefined();
186
+ });
187
+
188
+ it("filters invalid analysis types", () => {
189
+ const stepOutputs = {
190
+ "analyze-bug": { issues: 5 },
191
+ "analyze-invalid": { issues: 3 },
192
+ "analyze-and-fix-all": { container: true },
193
+ };
194
+
195
+ const result = extractAnalysisSteps(stepOutputs);
196
+
197
+ expect(result["analyze-bug"]).toBeDefined();
198
+ expect(result["analyze-invalid"]).toBeUndefined();
199
+ expect(result["analyze-and-fix-all"]).toBeUndefined();
200
+ });
201
+
202
+ it("returns empty for empty outputs", () => {
203
+ expect(extractAnalysisSteps({})).toEqual({});
204
+ });
205
+
206
+ it("recognizes all analysis types", () => {
207
+ const stepOutputs = {
208
+ "analyze-bug": { type: "bug" },
209
+ "analyze-debt": { type: "debt" },
210
+ "analyze-doc": { type: "doc" },
211
+ "analyze-security": { type: "security" },
212
+ "analyze-style": { type: "style" },
213
+ };
214
+
215
+ const result = extractAnalysisSteps(stepOutputs);
216
+ expect(Object.keys(result).length).toBe(5);
217
+ });
218
+ });
219
+
220
+ describe("extractFixSteps", () => {
221
+ it("extracts fix steps from outputs", () => {
222
+ const stepOutputs = {
223
+ "fix-bugs": { fixed: 3 },
224
+ "apply-fixes": { applied: 5 },
225
+ "other-step": { ignored: true },
226
+ };
227
+
228
+ const result = extractFixSteps(stepOutputs);
229
+
230
+ expect(result["fix-bugs"]).toBeDefined();
231
+ expect(result["apply-fixes"]).toBeDefined();
232
+ expect(result["other-step"]).toBeUndefined();
233
+ });
234
+
235
+ it("returns empty for empty outputs", () => {
236
+ expect(extractFixSteps({})).toEqual({});
237
+ });
238
+ });
239
+
240
+ describe("buildTemplateContext", () => {
241
+ it("builds basic template context", () => {
242
+ const context = buildTemplateContext(
243
+ "test-workflow",
244
+ "test-workflow-20260111-143000",
245
+ "2026-01-11T14:30:00Z",
246
+ "2026-01-11T15:00:00Z",
247
+ { step1: { result: "ok" } },
248
+ ["file1.py", "file2.py"],
249
+ ["feature/test"],
250
+ [{ number: 123, url: "https://github.com/test" }],
251
+ { var: "value" },
252
+ );
253
+
254
+ expect(context.workflow).toEqual({
255
+ name: "test-workflow",
256
+ id: "test-workflow-20260111-143000",
257
+ started_at: "2026-01-11T14:30:00Z",
258
+ completed_at: "2026-01-11T15:00:00Z",
259
+ });
260
+ expect(context.workflow_id).toBe("test-workflow-20260111-143000");
261
+ expect(context.steps).toEqual({ step1: { result: "ok" } });
262
+ expect(context.files_changed).toEqual(["file1.py", "file2.py"]);
263
+ expect(context.branches).toEqual(["feature/test"]);
264
+ expect((context.pull_requests as unknown[]).length).toBe(1);
265
+ expect(context.inputs).toEqual({ var: "value" });
266
+ });
267
+
268
+ it("extracts analysis steps into context", () => {
269
+ const context = buildTemplateContext(
270
+ "test",
271
+ "test-123",
272
+ "",
273
+ null,
274
+ {
275
+ "analyze-bug": { issues: 5 },
276
+ "other-step": { data: "ignored" },
277
+ },
278
+ [],
279
+ [],
280
+ [],
281
+ {},
282
+ );
283
+
284
+ const analysisSteps = context.analysis_steps as Record<string, unknown>;
285
+ expect(analysisSteps["analyze-bug"]).toBeDefined();
286
+ expect(analysisSteps["other-step"]).toBeUndefined();
287
+ });
288
+
289
+ it("extracts fix steps into context", () => {
290
+ const context = buildTemplateContext(
291
+ "test",
292
+ "test-123",
293
+ "",
294
+ null,
295
+ {
296
+ "fix-bugs": { fixed: 3 },
297
+ "apply-fixes": { applied: 2 },
298
+ },
299
+ [],
300
+ [],
301
+ [],
302
+ {},
303
+ );
304
+
305
+ const fixSteps = context.fix_steps as Record<string, unknown>;
306
+ expect(fixSteps["fix-bugs"]).toBeDefined();
307
+ expect(fixSteps["apply-fixes"]).toBeDefined();
308
+ });
309
+ });
310
+
311
+ describe("renderWorkflowOutput", () => {
312
+ it("creates output file from template", () => {
313
+ const templatesDir = path.join(tempDir, "templates");
314
+ mkdirSync(templatesDir, { recursive: true });
315
+ writeFileSync(path.join(templatesDir, "output.j2"), "Workflow: {{ workflow.name }}");
316
+
317
+ const outputPath = path.join(tempDir, "output", "result.md");
318
+ const context = { workflow: { name: "test-workflow" } };
319
+
320
+ renderWorkflowOutput("output.j2", outputPath, context, [templatesDir]);
321
+
322
+ expect(existsSync(outputPath)).toBe(true);
323
+ expect(readFileSync(outputPath, "utf-8")).toBe("Workflow: test-workflow");
324
+ });
325
+
326
+ it("creates necessary directories", () => {
327
+ const templatesDir = path.join(tempDir, "templates");
328
+ mkdirSync(templatesDir, { recursive: true });
329
+ writeFileSync(path.join(templatesDir, "output.j2"), "Content");
330
+
331
+ const outputPath = path.join(tempDir, "deep", "nested", "output.md");
332
+
333
+ renderWorkflowOutput("output.j2", outputPath, {}, [templatesDir]);
334
+
335
+ expect(existsSync(outputPath)).toBe(true);
336
+ });
337
+
338
+ it("renders with absolute template path", () => {
339
+ const templateFile = path.join(tempDir, "absolute_template.j2");
340
+ writeFileSync(templateFile, "Value: {{ value }}");
341
+
342
+ const outputPath = path.join(tempDir, "output.md");
343
+ const context = { value: "test" };
344
+
345
+ renderWorkflowOutput(templateFile, outputPath, context);
346
+
347
+ expect(existsSync(outputPath)).toBe(true);
348
+ expect(readFileSync(outputPath, "utf-8")).toBe("Value: test");
349
+ });
350
+ });