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
package/src/console.ts ADDED
@@ -0,0 +1,790 @@
1
+ /** Console output utilities for workflow execution. */
2
+
3
+ import type { Writable } from "node:stream";
4
+
5
+ // --- Enums and constants ---
6
+
7
+ export const OutputLevel = {
8
+ BASE: "base",
9
+ ALL: "all",
10
+ } as const;
11
+ export type OutputLevel = (typeof OutputLevel)[keyof typeof OutputLevel];
12
+
13
+ export const Color = {
14
+ RESET: "\x1b[0m",
15
+ BOLD: "\x1b[1m",
16
+ DIM: "\x1b[2m",
17
+ RED: "\x1b[31m",
18
+ GREEN: "\x1b[32m",
19
+ YELLOW: "\x1b[33m",
20
+ BLUE: "\x1b[34m",
21
+ MAGENTA: "\x1b[35m",
22
+ CYAN: "\x1b[36m",
23
+ WHITE: "\x1b[37m",
24
+ BRIGHT_RED: "\x1b[91m",
25
+ BRIGHT_GREEN: "\x1b[92m",
26
+ BRIGHT_YELLOW: "\x1b[93m",
27
+ BRIGHT_BLUE: "\x1b[94m",
28
+ BRIGHT_CYAN: "\x1b[96m",
29
+ } as const;
30
+ export type Color = (typeof Color)[keyof typeof Color];
31
+
32
+ /** Maximum characters to accumulate in BASE mode before truncating (1MB). */
33
+ const MAX_ACCUMULATED_TEXT = 1024 * 1024;
34
+
35
+ // --- Utility functions ---
36
+
37
+ export function supportsColor(): boolean {
38
+ if (typeof process.stdout.isTTY !== "boolean") {
39
+ return false;
40
+ }
41
+ return process.stdout.isTTY;
42
+ }
43
+
44
+ export function colorize(text: string, ...colors: Color[]): string {
45
+ if (!supportsColor()) {
46
+ return text;
47
+ }
48
+ const colorCodes = colors.join("");
49
+ return `${colorCodes}${text}${Color.RESET}`;
50
+ }
51
+
52
+ /**
53
+ * Format a model name for display (e.g., "claude-sonnet-4-5-20250929" -> "Sonnet 4.5").
54
+ * This is a simplified version; the full version lives in runner.ts.
55
+ * We accept an optional formatter to avoid circular imports.
56
+ */
57
+ type ModelFormatter = (model: string) => string | null;
58
+
59
+ // --- ParallelOutputHandler ---
60
+
61
+ export class ParallelOutputHandler {
62
+ stream: Writable;
63
+ level: OutputLevel;
64
+ private printFn: ((message: string) => void) | null;
65
+
66
+ private branches: string[] = [];
67
+ private branchIndex: Map<string, number> = new Map();
68
+ private branchDone: Map<string, boolean> = new Map();
69
+ private deferredMessages: string[] = [];
70
+ private linesPerBranch = 4;
71
+ private active = false;
72
+
73
+ // Per-branch accumulation (single-threaded, so we use maps instead of thread-local)
74
+ private currentBranch: string | null = null;
75
+ private accumulatedText = "";
76
+ private accumulatedRole = "";
77
+ private accumulatedModel: string | null = null;
78
+ private baseAccumulatedText = "";
79
+
80
+ // Message queue for ALL mode parallel
81
+ private messageQueue: Array<[string, string, string, string | null]> = [];
82
+
83
+ constructor(
84
+ stream: Writable = process.stdout,
85
+ level: OutputLevel = OutputLevel.BASE,
86
+ printFn?: (message: string) => void,
87
+ ) {
88
+ this.stream = stream;
89
+ this.level = level;
90
+ this.printFn = printFn ?? null;
91
+ }
92
+
93
+ private print(message: string): void {
94
+ if (this.printFn) {
95
+ this.printFn(message);
96
+ } else {
97
+ this.stream.write(`${message}\n`);
98
+ }
99
+ }
100
+
101
+ get isActive(): boolean {
102
+ return this.active;
103
+ }
104
+
105
+ get hasBranches(): boolean {
106
+ return this.branches.length > 0;
107
+ }
108
+
109
+ registerBranches(branches: string[]): void {
110
+ this.branches = [...branches];
111
+ this.branchIndex = new Map(branches.map((name, idx) => [name, idx]));
112
+ this.branchDone = new Map(branches.map((name) => [name, false]));
113
+ this.deferredMessages = [];
114
+
115
+ if (this.level === OutputLevel.BASE && supportsColor()) {
116
+ for (const branch of branches) {
117
+ const prefix = colorize(`[${branch}]`, Color.CYAN, Color.BOLD);
118
+ this.print(prefix);
119
+ const waiting = colorize("waiting...", Color.DIM);
120
+ this.print(` ${waiting}`);
121
+ this.print(""); // Message line 2
122
+ this.print(""); // Blank line
123
+ }
124
+ }
125
+ }
126
+
127
+ updateBranchLine(branch: string, text: string, maxWidth = 100): void {
128
+ if (!supportsColor() || !this.branchIndex.has(branch)) return;
129
+ if (this.branchDone.get(branch)) return;
130
+
131
+ const idx = this.branchIndex.get(branch) ?? 0;
132
+ const totalBranches = this.branches.length;
133
+ const totalLines = totalBranches * this.linesPerBranch;
134
+ const branchStartLine = idx * this.linesPerBranch;
135
+ const messageLine1 = branchStartLine + 1;
136
+ const messageLine2 = branchStartLine + 2;
137
+ const linesUpToMsg1 = totalLines - messageLine1;
138
+ const linesUpToMsg2 = totalLines - messageLine2;
139
+
140
+ const trimmed = text.trim();
141
+ let line1: string;
142
+ let line2 = "";
143
+ if (trimmed.length <= maxWidth) {
144
+ line1 = trimmed;
145
+ } else {
146
+ let breakPoint = trimmed.lastIndexOf(" ", maxWidth);
147
+ if (breakPoint === -1) breakPoint = maxWidth;
148
+ line1 = trimmed.slice(0, breakPoint).trim();
149
+ line2 = trimmed.slice(breakPoint).trim();
150
+ if (line2.length > maxWidth) {
151
+ line2 = `${line2.slice(0, maxWidth - 3)}...`;
152
+ }
153
+ }
154
+
155
+ const prefix = colorize("...", Color.DIM);
156
+ const w = this.stream;
157
+ w.write("\x1b[s"); // Save cursor
158
+ w.write(`\x1b[${linesUpToMsg1}A`); // Move up
159
+ w.write("\x1b[K"); // Clear line
160
+ w.write(` ${prefix} ${line1}`);
161
+ w.write(`\x1b[${linesUpToMsg1 - linesUpToMsg2}B`); // Move down to line 2
162
+ w.write("\r\x1b[K"); // Clear line
163
+ if (line2) {
164
+ w.write(` ${prefix} ${line2}`);
165
+ }
166
+ w.write("\x1b[u"); // Restore cursor
167
+ }
168
+
169
+ markBranchDone(branch: string, success = true): void {
170
+ if (!supportsColor() || !this.branchIndex.has(branch)) return;
171
+
172
+ this.branchDone.set(branch, true);
173
+ const idx = this.branchIndex.get(branch) ?? 0;
174
+ const totalBranches = this.branches.length;
175
+ const totalLines = totalBranches * this.linesPerBranch;
176
+ const branchStartLine = idx * this.linesPerBranch;
177
+ const linesUpToHeader = totalLines - branchStartLine;
178
+ const messageLine1 = branchStartLine + 1;
179
+ const messageLine2 = branchStartLine + 2;
180
+ const linesUpToMsg1 = totalLines - messageLine1;
181
+ const linesUpToMsg2 = totalLines - messageLine2;
182
+
183
+ const branchPrefix = colorize(`[${branch}]`, Color.CYAN, Color.BOLD);
184
+ const status = success
185
+ ? colorize("[Done]", Color.BRIGHT_GREEN, Color.BOLD)
186
+ : colorize("[Failed]", Color.BRIGHT_RED, Color.BOLD);
187
+
188
+ const w = this.stream;
189
+ w.write("\x1b[s");
190
+ w.write(`\x1b[${linesUpToHeader}A`);
191
+ w.write("\x1b[K");
192
+ w.write(`${branchPrefix} ${status}`);
193
+ w.write(`\x1b[${linesUpToHeader - linesUpToMsg1}B`);
194
+ w.write("\r\x1b[K");
195
+ w.write(`\x1b[${linesUpToMsg1 - linesUpToMsg2}B`);
196
+ w.write("\r\x1b[K");
197
+ w.write("\x1b[u");
198
+ }
199
+
200
+ private clearDisplay(): void {
201
+ if (this.branches.length === 0 || !supportsColor()) {
202
+ this.branches = [];
203
+ this.branchIndex = new Map();
204
+ this.branchDone = new Map();
205
+ return;
206
+ }
207
+
208
+ const totalLines = this.branches.length * this.linesPerBranch;
209
+ for (let i = 0; i < totalLines; i++) {
210
+ this.stream.write("\x1b[A\x1b[2K");
211
+ }
212
+
213
+ this.branches = [];
214
+ this.branchIndex = new Map();
215
+ this.branchDone = new Map();
216
+ }
217
+
218
+ enter(): void {
219
+ this.active = true;
220
+ if (this.level === OutputLevel.ALL) {
221
+ this.messageQueue = [];
222
+ }
223
+ }
224
+
225
+ exit(): void {
226
+ if (this.level === OutputLevel.BASE) {
227
+ if (this.branches.length > 0) {
228
+ this.clearDisplay();
229
+ }
230
+ for (const msg of this.deferredMessages) {
231
+ this.print(msg);
232
+ }
233
+ this.deferredMessages = [];
234
+ }
235
+
236
+ // Flush remaining messages in ALL mode
237
+ if (this.level === OutputLevel.ALL) {
238
+ for (const [branch, role, text, model] of this.messageQueue) {
239
+ this.printBranchMessage(branch, role, text, model);
240
+ }
241
+ this.messageQueue = [];
242
+ }
243
+
244
+ this.active = false;
245
+ }
246
+
247
+ deferMessage(message: string): void {
248
+ this.deferredMessages.push(message);
249
+ }
250
+
251
+ setBranch(branchName: string): void {
252
+ this.currentBranch = branchName;
253
+ this.accumulatedText = "";
254
+ this.accumulatedRole = "";
255
+ this.accumulatedModel = null;
256
+ this.baseAccumulatedText = "";
257
+ }
258
+
259
+ getCurrentBranch(): string | null {
260
+ return this.currentBranch;
261
+ }
262
+
263
+ enqueueCurrentMessage(): void {
264
+ if (
265
+ this.level === OutputLevel.ALL &&
266
+ this.active &&
267
+ this.accumulatedText.trim() &&
268
+ this.accumulatedRole &&
269
+ this.currentBranch
270
+ ) {
271
+ this.messageQueue.push([
272
+ this.currentBranch,
273
+ this.accumulatedRole,
274
+ this.accumulatedText.trim(),
275
+ this.accumulatedModel,
276
+ ]);
277
+ }
278
+
279
+ this.accumulatedText = "";
280
+ this.accumulatedRole = "";
281
+ this.accumulatedModel = null;
282
+ }
283
+
284
+ private printBranchMessage(
285
+ branch: string,
286
+ role: string,
287
+ text: string,
288
+ model: string | null,
289
+ formatModelName?: ModelFormatter,
290
+ ): void {
291
+ const formattedModel = formatModelName && model ? formatModelName(model) : null;
292
+ const branchPrefix = colorize(`[${branch}] `, Color.CYAN, Color.BOLD);
293
+
294
+ if (role === "user") {
295
+ const prefix = colorize(">", Color.BRIGHT_CYAN, Color.BOLD);
296
+ const label = colorize(" [user]", Color.DIM);
297
+ this.print(`\n${branchPrefix}${prefix}${label}`);
298
+ for (const line of text.split("\n")) {
299
+ const coloredLine = colorize(line, Color.GREEN);
300
+ this.print(` ${coloredLine}`);
301
+ }
302
+ } else {
303
+ const bullet = colorize("*", Color.BRIGHT_GREEN, Color.BOLD);
304
+ let modelLabel = "";
305
+ if (formattedModel) {
306
+ modelLabel = ` ${colorize(`[${formattedModel}]`, Color.DIM)}`;
307
+ }
308
+ const lines = text.split("\n");
309
+ this.print(`\n${branchPrefix}${bullet}${modelLabel} ${lines[0]}`);
310
+ for (const line of lines.slice(1)) {
311
+ this.print(` ${line}`);
312
+ }
313
+ }
314
+ }
315
+
316
+ accumulateText(text: string, role: string, model?: string | null): void {
317
+ if (this.accumulatedRole && this.accumulatedRole !== role && this.accumulatedText.trim()) {
318
+ this.enqueueCurrentMessage();
319
+ }
320
+
321
+ this.accumulatedText += text;
322
+ this.accumulatedRole = role;
323
+ if (model) {
324
+ this.accumulatedModel = model;
325
+ }
326
+ }
327
+
328
+ accumulateBaseText(text: string): string {
329
+ this.baseAccumulatedText += text;
330
+ const accumulated = this.baseAccumulatedText.trim();
331
+ if (accumulated) {
332
+ const lines = accumulated.split("\n");
333
+ return lines[lines.length - 1];
334
+ }
335
+ return "";
336
+ }
337
+ }
338
+
339
+ // --- ConsoleOutput ---
340
+
341
+ export class ConsoleOutput {
342
+ level: OutputLevel;
343
+ stream: Writable;
344
+ _baseAccumulatedText = "";
345
+ private baseLastDisplayLines = 0;
346
+ _parallelHandler: ParallelOutputHandler;
347
+ private formatModelName: ModelFormatter | null = null;
348
+
349
+ constructor(level: OutputLevel = OutputLevel.BASE, stream: Writable = process.stdout) {
350
+ this.level = level;
351
+ this.stream = stream;
352
+ this._parallelHandler = new ParallelOutputHandler(stream, level, (msg) => this.printLine(msg));
353
+ }
354
+
355
+ /** Set model name formatter to avoid circular imports with runner. */
356
+ setModelFormatter(fn: ModelFormatter): void {
357
+ this.formatModelName = fn;
358
+ }
359
+
360
+ private printLine(message: string, end = "\n"): void {
361
+ this.stream.write(`${message}${end}`);
362
+ }
363
+
364
+ private printInplace(message: string): void {
365
+ const lines = message.trim().split("\n");
366
+ const numLines = lines.length;
367
+
368
+ if (supportsColor()) {
369
+ if (this.baseLastDisplayLines > 0) {
370
+ this.stream.write(`\x1b[${this.baseLastDisplayLines}A`);
371
+ this.stream.write("\x1b[J");
372
+ }
373
+ const content = `${lines.join("\n")}\n`;
374
+ this.stream.write(content);
375
+ } else {
376
+ const lastLine = lines[lines.length - 1] ?? "";
377
+ const truncated = lastLine.length > 80 ? `${lastLine.slice(0, 80)}...` : lastLine;
378
+ this.printLine(` ... ${truncated}`);
379
+ }
380
+
381
+ this.baseLastDisplayLines = numLines;
382
+ }
383
+
384
+ private clearInplace(): void {
385
+ if (this.baseLastDisplayLines > 0 && supportsColor()) {
386
+ this.stream.write(`\x1b[${this.baseLastDisplayLines}A\x1b[J`);
387
+ }
388
+ this.baseLastDisplayLines = 0;
389
+ }
390
+
391
+ // --- Parallel mode delegation ---
392
+
393
+ registerParallelBranches(branches: string[]): void {
394
+ this._parallelHandler.registerBranches(branches);
395
+ }
396
+
397
+ enterParallelMode(): void {
398
+ this._parallelHandler.enter();
399
+ }
400
+
401
+ exitParallelMode(): void {
402
+ this._parallelHandler.exit();
403
+ }
404
+
405
+ setParallelBranch(branchName: string): void {
406
+ this._parallelHandler.setBranch(branchName);
407
+ }
408
+
409
+ flushParallelBranch(_branchName: string): void {
410
+ // No-op, kept for compatibility
411
+ }
412
+
413
+ markParallelBranchDone(branch: string, success = true): void {
414
+ this._parallelHandler.markBranchDone(branch, success);
415
+ }
416
+
417
+ private isParallelMode(): boolean {
418
+ return this._parallelHandler.isActive;
419
+ }
420
+
421
+ private shouldDeferMessage(): boolean {
422
+ return (
423
+ this._parallelHandler.isActive &&
424
+ this.level === OutputLevel.BASE &&
425
+ this._parallelHandler.hasBranches
426
+ );
427
+ }
428
+
429
+ private deferMessage(message: string): void {
430
+ this._parallelHandler.deferMessage(message);
431
+ }
432
+
433
+ // --- Workflow-level messages ---
434
+
435
+ workflowStart(workflowName: string, workflowId: string): void {
436
+ const header = colorize(`Workflow: ${workflowName}`, Color.BOLD, Color.BRIGHT_CYAN);
437
+ const idStr = colorize(`[${workflowId}]`, Color.DIM);
438
+ this.printLine(`\n${header} ${idStr}`);
439
+ this.printLine(colorize("-".repeat(50), Color.DIM));
440
+ }
441
+
442
+ workflowComplete(workflowName: string, status: string): void {
443
+ this.printLine(colorize("-".repeat(50), Color.DIM));
444
+ const statusStr =
445
+ status === "completed"
446
+ ? colorize("COMPLETED", Color.BOLD, Color.BRIGHT_GREEN)
447
+ : colorize(status.toUpperCase(), Color.BOLD, Color.BRIGHT_RED);
448
+ this.printLine(`Workflow ${workflowName}: ${statusStr}\n`);
449
+ }
450
+
451
+ // --- Step-level messages ---
452
+
453
+ stepStart(stepName: string, stepType?: string | null, model?: string | null): void {
454
+ let modelStr = "";
455
+ if (this.level === OutputLevel.BASE && model) {
456
+ const formattedModel = this.formatModelName ? this.formatModelName(model) : model;
457
+ if (formattedModel) {
458
+ modelStr = `${colorize(`[${formattedModel}] `, Color.DIM)}`;
459
+ }
460
+ }
461
+
462
+ const stepStr = colorize(`Step: ${stepName}`, Color.BOLD);
463
+ const typeStr = stepType ? colorize(`[${stepType}]`, Color.DIM) : "";
464
+ const formatted = `\n${modelStr}${stepStr} ${typeStr}`;
465
+ if (this.shouldDeferMessage()) {
466
+ this.deferMessage(formatted);
467
+ } else {
468
+ this.printLine(formatted);
469
+ }
470
+ }
471
+
472
+ stepComplete(stepName: string, summary?: string | null): void {
473
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
474
+ this.baseLastDisplayLines = 0;
475
+ this._baseAccumulatedText = "";
476
+ }
477
+
478
+ const check = colorize("[OK]", Color.BRIGHT_GREEN);
479
+ const name = colorize(stepName, Color.GREEN);
480
+ const mainLine = `${check} ${name} completed`;
481
+
482
+ const lines = [mainLine];
483
+ if (summary && this.level === OutputLevel.BASE) {
484
+ const summaryLines = summary.trim().split("\n");
485
+ for (const line of summaryLines.slice(0, 5)) {
486
+ const truncated = line.length > 200 ? `${line.slice(0, 200)}...` : line;
487
+ lines.push(colorize(` ${truncated}`, Color.DIM));
488
+ }
489
+ if (summaryLines.length > 5) {
490
+ lines.push(colorize(` ... (${summaryLines.length - 5} more lines)`, Color.DIM));
491
+ }
492
+ }
493
+
494
+ if (this.shouldDeferMessage()) {
495
+ const branch = this._parallelHandler.getCurrentBranch();
496
+ if (branch) {
497
+ this.markParallelBranchDone(branch);
498
+ }
499
+ for (const line of lines) {
500
+ this.deferMessage(line);
501
+ }
502
+ } else {
503
+ for (const line of lines) {
504
+ this.printLine(line);
505
+ }
506
+ }
507
+ }
508
+
509
+ stepFailed(stepName: string, error?: string | null): void {
510
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
511
+ this.baseLastDisplayLines = 0;
512
+ this._baseAccumulatedText = "";
513
+ }
514
+
515
+ const cross = colorize("[FAIL]", Color.BRIGHT_RED);
516
+ const name = colorize(stepName, Color.RED);
517
+ const mainLine = `${cross} ${name} failed`;
518
+
519
+ const lines = [mainLine];
520
+ if (error) {
521
+ const errorLines = error.trim().split("\n");
522
+ for (const line of errorLines.slice(0, 10)) {
523
+ const truncated = line.length > 200 ? `${line.slice(0, 200)}...` : line;
524
+ lines.push(colorize(` ${truncated}`, Color.RED));
525
+ }
526
+ if (errorLines.length > 10) {
527
+ lines.push(colorize(` ... (${errorLines.length - 10} more lines)`, Color.DIM));
528
+ }
529
+ }
530
+
531
+ if (this.shouldDeferMessage()) {
532
+ const branch = this._parallelHandler.getCurrentBranch();
533
+ if (branch) {
534
+ this.markParallelBranchDone(branch, false);
535
+ }
536
+ for (const line of lines) {
537
+ this.deferMessage(line);
538
+ }
539
+ } else {
540
+ for (const line of lines) {
541
+ this.printLine(line);
542
+ }
543
+ }
544
+ }
545
+
546
+ stepRetry(stepName: string, attempt: number, maxAttempts: number, error?: string | null): void {
547
+ const retry = colorize(`[RETRY ${attempt}/${maxAttempts}]`, Color.YELLOW);
548
+ const name = colorize(stepName, Color.YELLOW);
549
+ const mainLine = `${retry} ${name}`;
550
+
551
+ const lines = [mainLine];
552
+ if (error) {
553
+ const firstLine = error.trim().split("\n")[0].slice(0, 100);
554
+ lines.push(colorize(` Reason: ${firstLine}`, Color.DIM));
555
+ }
556
+
557
+ if (this.shouldDeferMessage()) {
558
+ for (const line of lines) {
559
+ this.deferMessage(line);
560
+ }
561
+ } else {
562
+ for (const line of lines) {
563
+ this.printLine(line);
564
+ }
565
+ }
566
+ }
567
+
568
+ // --- Ralph loop messages ---
569
+
570
+ ralphIterationStart(stepName: string, iteration: number, maxIterations: number): void {
571
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
572
+ this.baseLastDisplayLines = 0;
573
+ this._baseAccumulatedText = "";
574
+ }
575
+
576
+ const progress = colorize(`[${iteration}/${maxIterations}]`, Color.CYAN);
577
+ const name = colorize(stepName, Color.CYAN);
578
+ this.printLine(`${progress} ${name} iteration`);
579
+ }
580
+
581
+ ralphIteration(
582
+ _stepName: string,
583
+ _iteration: number,
584
+ _maxIterations: number,
585
+ summary?: string | null,
586
+ ): void {
587
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
588
+ return;
589
+ }
590
+
591
+ if (summary && this.level === OutputLevel.ALL) {
592
+ const summaryLines = summary.trim().split("\n");
593
+ for (const line of summaryLines.slice(0, 3)) {
594
+ const truncated = line.length > 150 ? `${line.slice(0, 150)}...` : line;
595
+ this.printLine(colorize(` ${truncated}`, Color.DIM));
596
+ }
597
+ }
598
+ }
599
+
600
+ ralphComplete(stepName: string, iteration: number, maxIterations: number): void {
601
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
602
+ this.baseLastDisplayLines = 0;
603
+ this._baseAccumulatedText = "";
604
+ }
605
+
606
+ const check = colorize("[OK]", Color.BRIGHT_GREEN);
607
+ const name = colorize(stepName, Color.GREEN);
608
+ this.printLine(`${check} ${name} completed at iteration ${iteration}/${maxIterations}`);
609
+ }
610
+
611
+ ralphMaxIterations(stepName: string, maxIterations: number): void {
612
+ if (this.level === OutputLevel.BASE && !this.isParallelMode()) {
613
+ this.baseLastDisplayLines = 0;
614
+ this._baseAccumulatedText = "";
615
+ }
616
+
617
+ const warn = colorize("[WARN]", Color.YELLOW);
618
+ const name = colorize(stepName, Color.YELLOW);
619
+ this.printLine(`${warn} ${name} reached max iterations (${maxIterations})`);
620
+ }
621
+
622
+ // --- Generic messages ---
623
+
624
+ info(message: string): void {
625
+ const infoPrefix = colorize("[INFO]", Color.BLUE);
626
+ const formatted = `${infoPrefix} ${message}`;
627
+ if (this.shouldDeferMessage()) {
628
+ this.deferMessage(formatted);
629
+ } else {
630
+ this.printLine(formatted);
631
+ }
632
+ }
633
+
634
+ warning(message: string): void {
635
+ const warn = colorize("[WARN]", Color.YELLOW);
636
+ const formatted = `${warn} ${message}`;
637
+ if (this.shouldDeferMessage()) {
638
+ this.deferMessage(formatted);
639
+ } else {
640
+ this.printLine(formatted);
641
+ }
642
+ }
643
+
644
+ error(message: string): void {
645
+ const err = colorize("[ERROR]", Color.BRIGHT_RED);
646
+ const formatted = `${err} ${message}`;
647
+ if (this.shouldDeferMessage()) {
648
+ this.deferMessage(formatted);
649
+ } else {
650
+ this.printLine(formatted);
651
+ }
652
+ }
653
+
654
+ // --- Streaming ---
655
+
656
+ streamText(text: string, role = "assistant", model?: string | null): void {
657
+ const formattedModel = this.formatModelName && model ? this.formatModelName(model) : null;
658
+
659
+ if (this.level === OutputLevel.ALL) {
660
+ if (!text || !text.trim()) return;
661
+
662
+ if (this.isParallelMode()) {
663
+ this._parallelHandler.accumulateText(text, role, model);
664
+ return;
665
+ }
666
+
667
+ if (role === "user") {
668
+ const prefix = colorize(">", Color.BRIGHT_CYAN, Color.BOLD);
669
+ const label = colorize(" [user]", Color.DIM);
670
+ this.printLine("");
671
+ this.printLine(`${prefix}${label}`);
672
+ for (const line of text.split("\n")) {
673
+ const coloredLine = colorize(line, Color.GREEN);
674
+ this.printLine(` ${coloredLine}`);
675
+ }
676
+ } else {
677
+ const bullet = colorize("*", Color.BRIGHT_GREEN, Color.BOLD);
678
+ let modelLabel = "";
679
+ if (formattedModel) {
680
+ modelLabel = ` ${colorize(`[${formattedModel}]`, Color.DIM)}`;
681
+ }
682
+ const lines = text.split("\n");
683
+ this.printLine("");
684
+ this.printLine(`${bullet}${modelLabel} ${lines[0]}`);
685
+ for (const line of lines.slice(1)) {
686
+ this.printLine(` ${line}`);
687
+ }
688
+ }
689
+ } else if (this.level === OutputLevel.BASE) {
690
+ if (role === "user") return;
691
+
692
+ if (this.isParallelMode()) {
693
+ const branch = this._parallelHandler.getCurrentBranch();
694
+ if (branch && this._parallelHandler.hasBranches) {
695
+ const lastLine = this._parallelHandler.accumulateBaseText(text);
696
+ if (lastLine) {
697
+ this._parallelHandler.updateBranchLine(branch, lastLine);
698
+ }
699
+ }
700
+ return;
701
+ }
702
+
703
+ this._baseAccumulatedText += text;
704
+ if (this._baseAccumulatedText.length > MAX_ACCUMULATED_TEXT) {
705
+ this._baseAccumulatedText = this._baseAccumulatedText.slice(-MAX_ACCUMULATED_TEXT);
706
+ }
707
+
708
+ if (this._baseAccumulatedText.trim()) {
709
+ const prefix = colorize("...", Color.DIM);
710
+ const lines = this._baseAccumulatedText.trim().split("\n");
711
+ const formattedLines = [`${prefix} ${lines[0]}`];
712
+ for (const line of lines.slice(1)) {
713
+ formattedLines.push(` ${line}`);
714
+ }
715
+ const displayText = formattedLines.join("\n");
716
+ this.printInplace(displayText);
717
+ }
718
+ }
719
+ }
720
+
721
+ streamComplete(): void {
722
+ if (this.level === OutputLevel.ALL && this.isParallelMode()) {
723
+ this._parallelHandler.enqueueCurrentMessage();
724
+ }
725
+ this._baseAccumulatedText = "";
726
+ }
727
+ }
728
+
729
+ // --- Standalone utility functions ---
730
+
731
+ export function extractJson(output: string): Record<string, unknown> | null {
732
+ if (!output || !output.trim()) return null;
733
+
734
+ const pattern = /```json\s*([\s\S]*?)```/g;
735
+ const matches = [...output.matchAll(pattern)];
736
+
737
+ if (matches.length === 0) return null;
738
+
739
+ const jsonStr = matches[matches.length - 1][1].trim();
740
+
741
+ try {
742
+ return JSON.parse(jsonStr) as Record<string, unknown>;
743
+ } catch {
744
+ return null;
745
+ }
746
+ }
747
+
748
+ export function extractSummary(output: string, maxLines = 5, maxChars = 500): string {
749
+ if (!output || !output.trim()) return "";
750
+
751
+ const trimmed = output.trim();
752
+
753
+ const summaryMarkers = [
754
+ "## Summary",
755
+ "### Summary",
756
+ "Summary:",
757
+ "Result:",
758
+ "Completed:",
759
+ "Done:",
760
+ ];
761
+
762
+ for (const marker of summaryMarkers) {
763
+ if (trimmed.includes(marker)) {
764
+ const idx = trimmed.indexOf(marker);
765
+ const summarySection = trimmed.slice(idx);
766
+ const lines = summarySection.split("\n");
767
+ const resultLines: string[] = [];
768
+ for (let i = 1; i < lines.length; i++) {
769
+ if (i > maxLines) break;
770
+ if (lines[i].startsWith("#") && i > 1) break;
771
+ resultLines.push(lines[i]);
772
+ }
773
+ if (resultLines.length > 0) {
774
+ return resultLines.join("\n").trim().slice(0, maxChars);
775
+ }
776
+ }
777
+ }
778
+
779
+ const nonEmptyLines = trimmed.split("\n").filter((line) => line.trim());
780
+ if (nonEmptyLines.length === 0) return "";
781
+
782
+ const lastLines = nonEmptyLines.slice(-maxLines);
783
+ let summary = lastLines.join("\n");
784
+
785
+ if (summary.length > maxChars) {
786
+ summary = `${summary.slice(0, maxChars)}...`;
787
+ }
788
+
789
+ return summary;
790
+ }