ralphie 1.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/dist/cli.js ADDED
@@ -0,0 +1,3296 @@
1
+ #!/usr/bin/env tsx
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/cli.tsx
6
+ import { render } from "ink";
7
+ import { Command } from "commander";
8
+ import { readFileSync as readFileSync6, existsSync as existsSync8, unlinkSync, copyFileSync as copyFileSync3 } from "fs";
9
+ import { resolve, dirname as dirname3, join as join9 } from "path";
10
+ import { fileURLToPath as fileURLToPath3 } from "url";
11
+
12
+ // src/App.tsx
13
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback2 } from "react";
14
+ import { Box as Box11, Text as Text10, useApp } from "ink";
15
+ import { StatusMessage } from "@inkjs/ui";
16
+
17
+ // src/components/IterationHeader.tsx
18
+ import { Box, Text } from "ink";
19
+ import { ProgressBar } from "@inkjs/ui";
20
+
21
+ // src/lib/colors.ts
22
+ var COLORS = {
23
+ cyan: "cyan",
24
+ green: "green",
25
+ yellow: "yellow",
26
+ red: "red",
27
+ magenta: "magenta",
28
+ gray: "gray",
29
+ white: "white"
30
+ };
31
+ var ELEMENT_COLORS = {
32
+ border: COLORS.cyan,
33
+ success: COLORS.green,
34
+ warning: COLORS.yellow,
35
+ error: COLORS.red,
36
+ pending: COLORS.cyan,
37
+ text: COLORS.white,
38
+ muted: COLORS.gray
39
+ };
40
+ var CATEGORY_COLORS = {
41
+ read: COLORS.cyan,
42
+ write: COLORS.yellow,
43
+ command: COLORS.magenta,
44
+ meta: COLORS.gray
45
+ };
46
+ var STATE_COLORS = {
47
+ active: COLORS.cyan,
48
+ done: COLORS.green,
49
+ error: COLORS.red
50
+ };
51
+
52
+ // src/components/IterationHeader.tsx
53
+ import { jsxDEV } from "react/jsx-dev-runtime";
54
+ function formatElapsedTime(totalSeconds) {
55
+ const hours = Math.floor(totalSeconds / 3600);
56
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
57
+ const seconds = Math.floor(totalSeconds % 60);
58
+ if (hours > 0) {
59
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
60
+ }
61
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
62
+ }
63
+ function IterationHeader({ current, total, elapsedSeconds }) {
64
+ const label = `Iteration ${current}/${total}`;
65
+ const elapsed = `${formatElapsedTime(elapsedSeconds)} elapsed`;
66
+ const progress = total > 1 ? Math.round((current - 1) / total * 100) : 0;
67
+ return /* @__PURE__ */ jsxDEV(Box, {
68
+ flexDirection: "column",
69
+ children: [
70
+ /* @__PURE__ */ jsxDEV(Box, {
71
+ children: [
72
+ /* @__PURE__ */ jsxDEV(Text, {
73
+ color: ELEMENT_COLORS.border,
74
+ children: "┌─ "
75
+ }, undefined, false, undefined, this),
76
+ /* @__PURE__ */ jsxDEV(Text, {
77
+ bold: true,
78
+ color: ELEMENT_COLORS.text,
79
+ children: label
80
+ }, undefined, false, undefined, this),
81
+ /* @__PURE__ */ jsxDEV(Text, {
82
+ color: ELEMENT_COLORS.border,
83
+ children: " ─── "
84
+ }, undefined, false, undefined, this),
85
+ /* @__PURE__ */ jsxDEV(Text, {
86
+ color: ELEMENT_COLORS.muted,
87
+ children: elapsed
88
+ }, undefined, false, undefined, this)
89
+ ]
90
+ }, undefined, true, undefined, this),
91
+ total > 1 && /* @__PURE__ */ jsxDEV(Box, {
92
+ marginLeft: 3,
93
+ children: /* @__PURE__ */ jsxDEV(ProgressBar, {
94
+ value: progress
95
+ }, undefined, false, undefined, this)
96
+ }, undefined, false, undefined, this)
97
+ ]
98
+ }, undefined, true, undefined, this);
99
+ }
100
+
101
+ // src/components/TaskTitle.tsx
102
+ import { Box as Box2, Text as Text2 } from "ink";
103
+
104
+ // src/hooks/usePulse.ts
105
+ import { useState, useEffect, useRef } from "react";
106
+ var DEFAULT_INTERVAL_MS = 500;
107
+ function usePulse(options = {}) {
108
+ const { intervalMs = DEFAULT_INTERVAL_MS, enabled = true } = options;
109
+ const [pulse, setPulse] = useState(true);
110
+ const intervalRef = useRef(null);
111
+ useEffect(() => {
112
+ if (!enabled) {
113
+ if (intervalRef.current) {
114
+ clearInterval(intervalRef.current);
115
+ intervalRef.current = null;
116
+ }
117
+ setPulse(true);
118
+ return;
119
+ }
120
+ intervalRef.current = setInterval(() => {
121
+ setPulse((prev) => !prev);
122
+ }, intervalMs);
123
+ return () => {
124
+ if (intervalRef.current) {
125
+ clearInterval(intervalRef.current);
126
+ intervalRef.current = null;
127
+ }
128
+ };
129
+ }, [intervalMs, enabled]);
130
+ return pulse;
131
+ }
132
+
133
+ // src/components/TaskTitle.tsx
134
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
135
+ function truncateText(text, maxLength) {
136
+ if (text.length <= maxLength) {
137
+ return text;
138
+ }
139
+ return text.slice(0, maxLength - 3) + "...";
140
+ }
141
+ function TaskTitle({ text, maxLength = 60, isPending = false }) {
142
+ const pulse = usePulse({ enabled: isPending });
143
+ const iconColor = isPending && !pulse ? ELEMENT_COLORS.muted : ELEMENT_COLORS.success;
144
+ if (!text) {
145
+ return /* @__PURE__ */ jsxDEV2(Box2, {
146
+ children: /* @__PURE__ */ jsxDEV2(Text2, {
147
+ color: ELEMENT_COLORS.border,
148
+ children: "│"
149
+ }, undefined, false, undefined, this)
150
+ }, undefined, false, undefined, this);
151
+ }
152
+ const displayText = truncateText(text.trim(), maxLength);
153
+ return /* @__PURE__ */ jsxDEV2(Box2, {
154
+ children: [
155
+ /* @__PURE__ */ jsxDEV2(Text2, {
156
+ color: ELEMENT_COLORS.border,
157
+ children: "│ "
158
+ }, undefined, false, undefined, this),
159
+ /* @__PURE__ */ jsxDEV2(Text2, {
160
+ color: iconColor,
161
+ children: "▶ "
162
+ }, undefined, false, undefined, this),
163
+ /* @__PURE__ */ jsxDEV2(Text2, {
164
+ color: ELEMENT_COLORS.text,
165
+ children: [
166
+ '"',
167
+ displayText,
168
+ '"'
169
+ ]
170
+ }, undefined, true, undefined, this)
171
+ ]
172
+ }, undefined, true, undefined, this);
173
+ }
174
+
175
+ // src/components/ActivityFeed.tsx
176
+ import { Box as Box7 } from "ink";
177
+
178
+ // src/components/ThoughtItem.tsx
179
+ import { Box as Box3, Text as Text3 } from "ink";
180
+ import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
181
+ function ThoughtItem({ item }) {
182
+ return /* @__PURE__ */ jsxDEV3(Box3, {
183
+ children: [
184
+ /* @__PURE__ */ jsxDEV3(Text3, {
185
+ color: ELEMENT_COLORS.border,
186
+ children: "│ "
187
+ }, undefined, false, undefined, this),
188
+ /* @__PURE__ */ jsxDEV3(Text3, {
189
+ color: ELEMENT_COLORS.text,
190
+ children: "● "
191
+ }, undefined, false, undefined, this),
192
+ /* @__PURE__ */ jsxDEV3(Text3, {
193
+ color: ELEMENT_COLORS.text,
194
+ children: item.text
195
+ }, undefined, false, undefined, this)
196
+ ]
197
+ }, undefined, true, undefined, this);
198
+ }
199
+
200
+ // src/components/ToolActivityItem.tsx
201
+ import { Box as Box5, Text as Text5 } from "ink";
202
+ import Spinner2 from "ink-spinner";
203
+
204
+ // src/lib/tool-categories.ts
205
+ var TOOL_CATEGORIES = {
206
+ Read: "read",
207
+ Grep: "read",
208
+ Glob: "read",
209
+ WebFetch: "read",
210
+ WebSearch: "read",
211
+ LSP: "read",
212
+ Edit: "write",
213
+ Write: "write",
214
+ NotebookEdit: "write",
215
+ Bash: "command",
216
+ TodoWrite: "meta",
217
+ Task: "meta",
218
+ AskUserQuestion: "meta",
219
+ EnterPlanMode: "meta",
220
+ ExitPlanMode: "meta"
221
+ };
222
+ function getToolCategory(toolName) {
223
+ return TOOL_CATEGORIES[toolName] ?? "meta";
224
+ }
225
+
226
+ // src/components/ToolItem.tsx
227
+ import { Box as Box4, Text as Text4 } from "ink";
228
+ import Spinner from "ink-spinner";
229
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
230
+ function formatDuration(ms) {
231
+ const seconds = ms / 1000;
232
+ return `${seconds.toFixed(1)}s`;
233
+ }
234
+
235
+ // src/components/ToolActivityItem.tsx
236
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
237
+ function ToolStartItem({ item }) {
238
+ const category = getToolCategory(item.toolName);
239
+ const color = CATEGORY_COLORS[category];
240
+ return /* @__PURE__ */ jsxDEV5(Box5, {
241
+ children: [
242
+ /* @__PURE__ */ jsxDEV5(Text5, {
243
+ color: ELEMENT_COLORS.border,
244
+ children: "│ "
245
+ }, undefined, false, undefined, this),
246
+ /* @__PURE__ */ jsxDEV5(Text5, {
247
+ color,
248
+ children: /* @__PURE__ */ jsxDEV5(Spinner2, {
249
+ type: "dots"
250
+ }, undefined, false, undefined, this)
251
+ }, undefined, false, undefined, this),
252
+ /* @__PURE__ */ jsxDEV5(Text5, {
253
+ children: " "
254
+ }, undefined, false, undefined, this),
255
+ /* @__PURE__ */ jsxDEV5(Text5, {
256
+ color: ELEMENT_COLORS.text,
257
+ children: item.displayName
258
+ }, undefined, false, undefined, this)
259
+ ]
260
+ }, undefined, true, undefined, this);
261
+ }
262
+ function ToolCompleteItem({ item }) {
263
+ const icon = item.isError ? "✗" : "✓";
264
+ const iconColor = item.isError ? ELEMENT_COLORS.error : ELEMENT_COLORS.success;
265
+ const textColor = item.isError ? ELEMENT_COLORS.error : ELEMENT_COLORS.text;
266
+ const duration = formatDuration(item.durationMs);
267
+ return /* @__PURE__ */ jsxDEV5(Box5, {
268
+ children: [
269
+ /* @__PURE__ */ jsxDEV5(Text5, {
270
+ color: ELEMENT_COLORS.border,
271
+ children: "│ "
272
+ }, undefined, false, undefined, this),
273
+ /* @__PURE__ */ jsxDEV5(Text5, {
274
+ color: iconColor,
275
+ children: icon
276
+ }, undefined, false, undefined, this),
277
+ /* @__PURE__ */ jsxDEV5(Text5, {
278
+ children: " "
279
+ }, undefined, false, undefined, this),
280
+ /* @__PURE__ */ jsxDEV5(Text5, {
281
+ color: textColor,
282
+ children: item.displayName
283
+ }, undefined, false, undefined, this),
284
+ /* @__PURE__ */ jsxDEV5(Text5, {
285
+ color: ELEMENT_COLORS.muted,
286
+ children: [
287
+ " (",
288
+ duration,
289
+ ")"
290
+ ]
291
+ }, undefined, true, undefined, this)
292
+ ]
293
+ }, undefined, true, undefined, this);
294
+ }
295
+
296
+ // src/components/CommitItem.tsx
297
+ import { Box as Box6, Text as Text6 } from "ink";
298
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
299
+ function CommitItem({ item }) {
300
+ const shortHash = item.hash.slice(0, 7);
301
+ return /* @__PURE__ */ jsxDEV6(Box6, {
302
+ children: [
303
+ /* @__PURE__ */ jsxDEV6(Text6, {
304
+ color: ELEMENT_COLORS.border,
305
+ children: "│ "
306
+ }, undefined, false, undefined, this),
307
+ /* @__PURE__ */ jsxDEV6(Text6, {
308
+ color: ELEMENT_COLORS.success,
309
+ children: "✓ "
310
+ }, undefined, false, undefined, this),
311
+ /* @__PURE__ */ jsxDEV6(Text6, {
312
+ color: ELEMENT_COLORS.success,
313
+ children: shortHash
314
+ }, undefined, false, undefined, this),
315
+ /* @__PURE__ */ jsxDEV6(Text6, {
316
+ color: ELEMENT_COLORS.muted,
317
+ children: " - "
318
+ }, undefined, false, undefined, this),
319
+ /* @__PURE__ */ jsxDEV6(Text6, {
320
+ color: ELEMENT_COLORS.text,
321
+ children: item.message
322
+ }, undefined, false, undefined, this)
323
+ ]
324
+ }, undefined, true, undefined, this);
325
+ }
326
+
327
+ // src/components/ActivityFeed.tsx
328
+ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
329
+ function renderActivityItem(item) {
330
+ switch (item.type) {
331
+ case "thought":
332
+ return /* @__PURE__ */ jsxDEV7(ThoughtItem, {
333
+ item
334
+ }, undefined, false, undefined, this);
335
+ case "tool_start":
336
+ return /* @__PURE__ */ jsxDEV7(ToolStartItem, {
337
+ item
338
+ }, undefined, false, undefined, this);
339
+ case "tool_complete":
340
+ return /* @__PURE__ */ jsxDEV7(ToolCompleteItem, {
341
+ item
342
+ }, undefined, false, undefined, this);
343
+ case "commit":
344
+ return /* @__PURE__ */ jsxDEV7(CommitItem, {
345
+ item
346
+ }, undefined, false, undefined, this);
347
+ }
348
+ }
349
+ function filterCompletedToolStarts(items) {
350
+ const completedToolIds = new Set;
351
+ for (const item of items) {
352
+ if (item.type === "tool_complete") {
353
+ completedToolIds.add(item.toolUseId);
354
+ }
355
+ }
356
+ return items.filter((item) => {
357
+ if (item.type === "tool_start") {
358
+ return !completedToolIds.has(item.toolUseId);
359
+ }
360
+ return true;
361
+ });
362
+ }
363
+ function ActivityFeed({ activityLog, maxItems = 20 }) {
364
+ if (activityLog.length === 0) {
365
+ return null;
366
+ }
367
+ const filteredItems = filterCompletedToolStarts(activityLog);
368
+ const displayItems = filteredItems.length > maxItems ? filteredItems.slice(-maxItems) : filteredItems;
369
+ return /* @__PURE__ */ jsxDEV7(Box7, {
370
+ flexDirection: "column",
371
+ children: displayItems.map((item, index) => /* @__PURE__ */ jsxDEV7(Box7, {
372
+ children: renderActivityItem(item)
373
+ }, `${item.type}-${item.timestamp}-${index}`, false, undefined, this))
374
+ }, undefined, false, undefined, this);
375
+ }
376
+
377
+ // src/components/PhaseIndicator.tsx
378
+ import { Box as Box8, Text as Text7 } from "ink";
379
+ import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
380
+ var PHASE_ICONS = {
381
+ idle: "○",
382
+ reading: "◐",
383
+ editing: "✎",
384
+ running: "⚡",
385
+ thinking: "●",
386
+ done: "✓"
387
+ };
388
+ var PHASE_LABELS = {
389
+ idle: "Waiting",
390
+ reading: "Reading",
391
+ editing: "Editing",
392
+ running: "Running",
393
+ thinking: "Thinking",
394
+ done: "Done"
395
+ };
396
+ function getPhaseIcon(phase) {
397
+ return PHASE_ICONS[phase];
398
+ }
399
+ function getPhaseDisplayLabel(phase) {
400
+ return PHASE_LABELS[phase];
401
+ }
402
+ function getPhaseColor(phase, isPulseBright) {
403
+ if (phase === "done")
404
+ return COLORS.green;
405
+ if (phase === "idle")
406
+ return COLORS.gray;
407
+ return isPulseBright ? COLORS.cyan : COLORS.gray;
408
+ }
409
+ function PhaseIndicator({ phase }) {
410
+ const isActive = phase !== "done" && phase !== "idle";
411
+ const pulse = usePulse({ enabled: isActive });
412
+ const icon = getPhaseIcon(phase);
413
+ const label = getPhaseDisplayLabel(phase);
414
+ const color = getPhaseColor(phase, pulse);
415
+ return /* @__PURE__ */ jsxDEV8(Box8, {
416
+ children: [
417
+ /* @__PURE__ */ jsxDEV8(Text7, {
418
+ color: ELEMENT_COLORS.border,
419
+ children: "│ "
420
+ }, undefined, false, undefined, this),
421
+ /* @__PURE__ */ jsxDEV8(Text7, {
422
+ color,
423
+ children: icon
424
+ }, undefined, false, undefined, this),
425
+ /* @__PURE__ */ jsxDEV8(Text7, {
426
+ children: " "
427
+ }, undefined, false, undefined, this),
428
+ /* @__PURE__ */ jsxDEV8(Text7, {
429
+ color,
430
+ children: label
431
+ }, undefined, false, undefined, this)
432
+ ]
433
+ }, undefined, true, undefined, this);
434
+ }
435
+
436
+ // src/components/StatusBar.tsx
437
+ import { Box as Box9, Text as Text8 } from "ink";
438
+ import { jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime";
439
+ var PHASE_LABELS2 = {
440
+ idle: "Waiting...",
441
+ reading: "Reading...",
442
+ editing: "Editing...",
443
+ running: "Running...",
444
+ thinking: "Thinking...",
445
+ done: "Done"
446
+ };
447
+ function getPhaseLabel(phase) {
448
+ return PHASE_LABELS2[phase];
449
+ }
450
+ function formatCommitInfo(commit) {
451
+ const shortHash = commit.hash.slice(0, 7);
452
+ return `${shortHash} - ${commit.message}`;
453
+ }
454
+ function StatusBar({ phase, elapsedSeconds, summary, lastCommit }) {
455
+ const phaseLabel = getPhaseLabel(phase);
456
+ const elapsed = formatElapsedTime(elapsedSeconds);
457
+ const displayText = summary ?? `${phaseLabel} (${elapsed})`;
458
+ const minWidth = 50;
459
+ const contentLength = displayText.length + 4;
460
+ const dashCount = Math.max(4, minWidth - contentLength);
461
+ const dashes = "─".repeat(dashCount);
462
+ const textColor = phase === "done" ? ELEMENT_COLORS.success : ELEMENT_COLORS.text;
463
+ return /* @__PURE__ */ jsxDEV9(Box9, {
464
+ flexDirection: "column",
465
+ children: [
466
+ lastCommit && /* @__PURE__ */ jsxDEV9(Box9, {
467
+ children: [
468
+ /* @__PURE__ */ jsxDEV9(Text8, {
469
+ color: ELEMENT_COLORS.border,
470
+ children: "│ "
471
+ }, undefined, false, undefined, this),
472
+ /* @__PURE__ */ jsxDEV9(Text8, {
473
+ color: ELEMENT_COLORS.success,
474
+ children: "✓ "
475
+ }, undefined, false, undefined, this),
476
+ /* @__PURE__ */ jsxDEV9(Text8, {
477
+ color: ELEMENT_COLORS.muted,
478
+ children: formatCommitInfo(lastCommit)
479
+ }, undefined, false, undefined, this)
480
+ ]
481
+ }, undefined, true, undefined, this),
482
+ /* @__PURE__ */ jsxDEV9(Box9, {
483
+ children: [
484
+ /* @__PURE__ */ jsxDEV9(Text8, {
485
+ color: ELEMENT_COLORS.border,
486
+ children: "└─ "
487
+ }, undefined, false, undefined, this),
488
+ /* @__PURE__ */ jsxDEV9(Text8, {
489
+ bold: true,
490
+ color: textColor,
491
+ children: displayText
492
+ }, undefined, false, undefined, this),
493
+ /* @__PURE__ */ jsxDEV9(Text8, {
494
+ color: ELEMENT_COLORS.border,
495
+ children: [
496
+ " ",
497
+ dashes
498
+ ]
499
+ }, undefined, true, undefined, this)
500
+ ]
501
+ }, undefined, true, undefined, this)
502
+ ]
503
+ }, undefined, true, undefined, this);
504
+ }
505
+
506
+ // src/components/CompletedIterationsList.tsx
507
+ import { Box as Box10, Text as Text9, Static } from "ink";
508
+ import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime";
509
+ function formatDuration2(ms) {
510
+ const seconds = Math.floor(ms / 1000);
511
+ if (seconds < 60) {
512
+ return `${seconds}s`;
513
+ }
514
+ const minutes = Math.floor(seconds / 60);
515
+ const remainingSeconds = seconds % 60;
516
+ return `${minutes}m ${remainingSeconds}s`;
517
+ }
518
+ function formatCost(costUsd) {
519
+ if (costUsd === null)
520
+ return "-";
521
+ if (costUsd < 0.01)
522
+ return "<$0.01";
523
+ return `$${costUsd.toFixed(2)}`;
524
+ }
525
+ function formatTokens(usage) {
526
+ if (usage === null)
527
+ return "";
528
+ const total = usage.inputTokens + usage.outputTokens;
529
+ if (total < 1000)
530
+ return `${total}`;
531
+ return `${(total / 1000).toFixed(1)}k`.replace(".0k", "k");
532
+ }
533
+ function truncateText2(text, maxLength) {
534
+ if (text.length <= maxLength)
535
+ return text;
536
+ return text.slice(0, maxLength - 3) + "...";
537
+ }
538
+ function CompletedIterationsList({ results }) {
539
+ if (results.length === 0)
540
+ return null;
541
+ return /* @__PURE__ */ jsxDEV10(Static, {
542
+ items: results,
543
+ children: (result, index) => /* @__PURE__ */ jsxDEV10(Box10, {
544
+ flexDirection: "column",
545
+ children: [
546
+ index === 0 && /* @__PURE__ */ jsxDEV10(Box10, {
547
+ children: /* @__PURE__ */ jsxDEV10(Text9, {
548
+ color: "cyan",
549
+ bold: true,
550
+ children: "Completed:"
551
+ }, undefined, false, undefined, this)
552
+ }, undefined, false, undefined, this),
553
+ /* @__PURE__ */ jsxDEV10(Box10, {
554
+ children: [
555
+ /* @__PURE__ */ jsxDEV10(Text9, {
556
+ color: result.error ? "red" : "green",
557
+ children: result.error ? "✗" : "✓"
558
+ }, undefined, false, undefined, this),
559
+ /* @__PURE__ */ jsxDEV10(Text9, {
560
+ color: "cyan",
561
+ children: [
562
+ " ",
563
+ result.taskNumber ?? result.iteration,
564
+ ". "
565
+ ]
566
+ }, undefined, true, undefined, this),
567
+ result.phaseName && /* @__PURE__ */ jsxDEV10(Text9, {
568
+ color: "yellow",
569
+ children: [
570
+ "[",
571
+ result.phaseName,
572
+ "] "
573
+ ]
574
+ }, undefined, true, undefined, this),
575
+ /* @__PURE__ */ jsxDEV10(Text9, {
576
+ color: "white",
577
+ children: truncateText2(result.specTaskText ?? result.taskText ?? "Unknown task", result.phaseName ? 30 : 45)
578
+ }, undefined, false, undefined, this),
579
+ /* @__PURE__ */ jsxDEV10(Text9, {
580
+ color: "gray",
581
+ children: [
582
+ " ",
583
+ "(",
584
+ formatDuration2(result.durationMs),
585
+ ", ",
586
+ formatTokens(result.usage),
587
+ result.usage ? ", " : "",
588
+ formatCost(result.costUsd),
589
+ ")"
590
+ ]
591
+ }, undefined, true, undefined, this)
592
+ ]
593
+ }, undefined, true, undefined, this)
594
+ ]
595
+ }, result.iteration, true, undefined, this)
596
+ }, undefined, false, undefined, this);
597
+ }
598
+
599
+ // src/hooks/useHarnessStream.ts
600
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef2, useCallback } from "react";
601
+ // src/lib/harness/claude.ts
602
+ import { query } from "@anthropic-ai/claude-agent-sdk";
603
+ var claudeHarness = {
604
+ name: "claude",
605
+ async run(prompt, options, onEvent) {
606
+ const startTime = Date.now();
607
+ if (!process.env.ANTHROPIC_API_KEY) {
608
+ const errorMessage = "Missing ANTHROPIC_API_KEY environment variable. Set it with: export ANTHROPIC_API_KEY=sk-ant-...";
609
+ onEvent({ type: "error", message: errorMessage });
610
+ return {
611
+ success: false,
612
+ durationMs: Date.now() - startTime,
613
+ error: errorMessage
614
+ };
615
+ }
616
+ try {
617
+ const queryResult = query({
618
+ prompt,
619
+ options: {
620
+ cwd: options.cwd,
621
+ permissionMode: "bypassPermissions",
622
+ allowedTools: options.allowedTools,
623
+ model: options.model,
624
+ systemPrompt: options.systemPrompt
625
+ }
626
+ });
627
+ let result = {
628
+ success: false,
629
+ durationMs: 0,
630
+ error: "No result received"
631
+ };
632
+ for await (const message of queryResult) {
633
+ if (message.type === "assistant") {
634
+ for (const block of message.message.content) {
635
+ if (block.type === "tool_use") {
636
+ onEvent({
637
+ type: "tool_start",
638
+ name: block.name,
639
+ input: JSON.stringify(block.input)
640
+ });
641
+ } else if (block.type === "text") {
642
+ onEvent({
643
+ type: "message",
644
+ text: block.text
645
+ });
646
+ } else if (block.type === "thinking") {
647
+ onEvent({
648
+ type: "thinking",
649
+ text: block.thinking
650
+ });
651
+ }
652
+ }
653
+ } else if (message.type === "user") {
654
+ for (const block of message.message.content) {
655
+ if (block.type === "tool_result") {
656
+ const toolResult = block;
657
+ onEvent({
658
+ type: "tool_end",
659
+ name: toolResult.tool_use_id,
660
+ output: typeof toolResult.content === "string" ? toolResult.content : JSON.stringify(toolResult.content),
661
+ error: toolResult.is_error
662
+ });
663
+ }
664
+ }
665
+ } else if (message.type === "result") {
666
+ if (message.subtype === "success") {
667
+ result = {
668
+ success: true,
669
+ durationMs: message.duration_ms,
670
+ costUsd: message.total_cost_usd,
671
+ usage: {
672
+ inputTokens: message.usage.input_tokens,
673
+ outputTokens: message.usage.output_tokens
674
+ },
675
+ output: message.result
676
+ };
677
+ } else {
678
+ const errorMsg = message;
679
+ result = {
680
+ success: false,
681
+ durationMs: message.duration_ms,
682
+ costUsd: message.total_cost_usd,
683
+ usage: {
684
+ inputTokens: message.usage.input_tokens,
685
+ outputTokens: message.usage.output_tokens
686
+ },
687
+ error: errorMsg.errors?.[0] ?? "Unknown error"
688
+ };
689
+ }
690
+ }
691
+ }
692
+ return result;
693
+ } catch (error) {
694
+ const errorMessage = error instanceof Error ? error.message : String(error);
695
+ onEvent({ type: "error", message: errorMessage });
696
+ return {
697
+ success: false,
698
+ durationMs: Date.now() - startTime,
699
+ error: errorMessage
700
+ };
701
+ }
702
+ }
703
+ };
704
+
705
+ // src/lib/harness/codex.ts
706
+ import { Codex } from "@openai/codex-sdk";
707
+ var codexHarness = {
708
+ name: "codex",
709
+ async run(prompt, options, onEvent) {
710
+ const startTime = Date.now();
711
+ if (!process.env.OPENAI_API_KEY) {
712
+ const errorMessage = "Missing OPENAI_API_KEY environment variable. Set it with: export OPENAI_API_KEY=sk-...";
713
+ onEvent({ type: "error", message: errorMessage });
714
+ return {
715
+ success: false,
716
+ durationMs: Date.now() - startTime,
717
+ error: errorMessage
718
+ };
719
+ }
720
+ try {
721
+ const codex = new Codex;
722
+ const thread = codex.startThread({
723
+ workingDirectory: options.cwd,
724
+ model: options.model,
725
+ approvalPolicy: "never",
726
+ sandboxMode: "workspace-write"
727
+ });
728
+ const { events } = await thread.runStreamed(prompt);
729
+ let result = {
730
+ success: false,
731
+ durationMs: 0,
732
+ error: "No result received"
733
+ };
734
+ let collectedOutput = "";
735
+ for await (const event of events) {
736
+ switch (event.type) {
737
+ case "item.started":
738
+ case "item.updated": {
739
+ const item = event.item;
740
+ if (item.type === "command_execution") {
741
+ onEvent({
742
+ type: "tool_start",
743
+ name: "Bash",
744
+ input: item.command
745
+ });
746
+ } else if (item.type === "mcp_tool_call") {
747
+ onEvent({
748
+ type: "tool_start",
749
+ name: item.tool,
750
+ input: JSON.stringify(item.arguments)
751
+ });
752
+ } else if (item.type === "reasoning") {
753
+ onEvent({
754
+ type: "thinking",
755
+ text: item.text
756
+ });
757
+ } else if (item.type === "agent_message") {
758
+ onEvent({
759
+ type: "message",
760
+ text: item.text
761
+ });
762
+ }
763
+ break;
764
+ }
765
+ case "item.completed": {
766
+ const item = event.item;
767
+ if (item.type === "command_execution") {
768
+ onEvent({
769
+ type: "tool_end",
770
+ name: "Bash",
771
+ output: item.aggregated_output,
772
+ error: item.status === "failed"
773
+ });
774
+ } else if (item.type === "mcp_tool_call") {
775
+ onEvent({
776
+ type: "tool_end",
777
+ name: item.tool,
778
+ output: item.result ? JSON.stringify(item.result.content) : item.error?.message,
779
+ error: item.status === "failed"
780
+ });
781
+ } else if (item.type === "file_change") {
782
+ onEvent({
783
+ type: "tool_end",
784
+ name: "FileChange",
785
+ output: item.changes.map((c) => `${c.kind}: ${c.path}`).join(`
786
+ `),
787
+ error: item.status === "failed"
788
+ });
789
+ } else if (item.type === "agent_message") {
790
+ collectedOutput = item.text;
791
+ } else if (item.type === "error") {
792
+ onEvent({
793
+ type: "error",
794
+ message: item.message
795
+ });
796
+ }
797
+ break;
798
+ }
799
+ case "turn.completed": {
800
+ result = {
801
+ success: true,
802
+ durationMs: Date.now() - startTime,
803
+ usage: event.usage ? {
804
+ inputTokens: event.usage.input_tokens,
805
+ outputTokens: event.usage.output_tokens
806
+ } : undefined,
807
+ output: collectedOutput || undefined
808
+ };
809
+ break;
810
+ }
811
+ case "turn.failed": {
812
+ result = {
813
+ success: false,
814
+ durationMs: Date.now() - startTime,
815
+ error: event.error.message
816
+ };
817
+ break;
818
+ }
819
+ case "error": {
820
+ onEvent({
821
+ type: "error",
822
+ message: event.message
823
+ });
824
+ result = {
825
+ success: false,
826
+ durationMs: Date.now() - startTime,
827
+ error: event.message
828
+ };
829
+ break;
830
+ }
831
+ }
832
+ }
833
+ return result;
834
+ } catch (error) {
835
+ const errorMessage = error instanceof Error ? error.message : String(error);
836
+ onEvent({ type: "error", message: errorMessage });
837
+ return {
838
+ success: false,
839
+ durationMs: Date.now() - startTime,
840
+ error: errorMessage
841
+ };
842
+ }
843
+ }
844
+ };
845
+
846
+ // src/lib/harness/index.ts
847
+ function getHarness(name = "claude") {
848
+ switch (name) {
849
+ case "claude":
850
+ return claudeHarness;
851
+ case "codex":
852
+ return codexHarness;
853
+ }
854
+ }
855
+
856
+ // src/hooks/useHarnessStream.ts
857
+ function useHarnessStream(options) {
858
+ const {
859
+ prompt,
860
+ cwd,
861
+ harness: harnessName = "claude",
862
+ model,
863
+ iteration = 1,
864
+ totalIterations = 1
865
+ } = options;
866
+ const [state, setState] = useState2(() => ({
867
+ phase: "idle",
868
+ taskText: null,
869
+ activeTools: [],
870
+ toolGroups: [],
871
+ stats: {
872
+ toolsStarted: 0,
873
+ toolsCompleted: 0,
874
+ toolsErrored: 0,
875
+ reads: 0,
876
+ writes: 0,
877
+ commands: 0,
878
+ metaOps: 0
879
+ },
880
+ elapsedMs: 0,
881
+ result: null,
882
+ error: null,
883
+ isRunning: false,
884
+ activityLog: [],
885
+ lastCommit: null
886
+ }));
887
+ const mountedRef = useRef2(true);
888
+ const startTimeRef = useRef2(Date.now());
889
+ const toolIdRef = useRef2(0);
890
+ const activeToolsMapRef = useRef2(new Map);
891
+ const handleEvent = useCallback((event) => {
892
+ if (!mountedRef.current)
893
+ return;
894
+ setState((prev) => {
895
+ switch (event.type) {
896
+ case "tool_start": {
897
+ const toolId = `tool-${toolIdRef.current++}`;
898
+ const category = getToolCategory(event.name);
899
+ const displayName = event.input ?? event.name;
900
+ const activeTool = {
901
+ id: toolId,
902
+ name: event.name,
903
+ category,
904
+ startTime: Date.now(),
905
+ input: event.input ? { raw: event.input } : {}
906
+ };
907
+ activeToolsMapRef.current.set(toolId, activeTool);
908
+ const newStats = { ...prev.stats, toolsStarted: prev.stats.toolsStarted + 1 };
909
+ if (category === "read")
910
+ newStats.reads++;
911
+ else if (category === "write")
912
+ newStats.writes++;
913
+ else if (category === "command")
914
+ newStats.commands++;
915
+ else
916
+ newStats.metaOps++;
917
+ const newActivity = {
918
+ type: "tool_start",
919
+ toolUseId: toolId,
920
+ toolName: event.name,
921
+ displayName,
922
+ timestamp: Date.now()
923
+ };
924
+ return {
925
+ ...prev,
926
+ phase: category === "read" ? "reading" : category === "write" ? "editing" : "running",
927
+ activeTools: Array.from(activeToolsMapRef.current.values()),
928
+ stats: newStats,
929
+ activityLog: [...prev.activityLog, newActivity]
930
+ };
931
+ }
932
+ case "tool_end": {
933
+ const completedTool = Array.from(activeToolsMapRef.current.values()).find((t) => t.name === event.name);
934
+ const toolUseId = completedTool?.id ?? `tool-unknown-${Date.now()}`;
935
+ if (completedTool) {
936
+ activeToolsMapRef.current.delete(completedTool.id);
937
+ }
938
+ const newStats = {
939
+ ...prev.stats,
940
+ toolsCompleted: prev.stats.toolsCompleted + 1,
941
+ toolsErrored: event.error ? prev.stats.toolsErrored + 1 : prev.stats.toolsErrored
942
+ };
943
+ const newActivity = {
944
+ type: "tool_complete",
945
+ toolUseId,
946
+ toolName: event.name,
947
+ displayName: event.name,
948
+ durationMs: completedTool ? Date.now() - completedTool.startTime : 0,
949
+ isError: event.error ?? false,
950
+ timestamp: Date.now()
951
+ };
952
+ let newCommit = prev.lastCommit;
953
+ if (event.name === "Bash" && event.output) {
954
+ const commitMatch = event.output.match(/\[[\w-]+\s+([a-f0-9]{7,40})\]\s+(.+)/);
955
+ if (commitMatch) {
956
+ newCommit = { hash: commitMatch[1], message: commitMatch[2] };
957
+ }
958
+ }
959
+ return {
960
+ ...prev,
961
+ phase: activeToolsMapRef.current.size > 0 ? prev.phase : "thinking",
962
+ activeTools: Array.from(activeToolsMapRef.current.values()),
963
+ stats: newStats,
964
+ activityLog: [...prev.activityLog, newActivity],
965
+ lastCommit: newCommit
966
+ };
967
+ }
968
+ case "thinking": {
969
+ const newActivity = {
970
+ type: "thought",
971
+ text: event.text,
972
+ timestamp: Date.now()
973
+ };
974
+ return {
975
+ ...prev,
976
+ phase: "thinking",
977
+ taskText: prev.taskText ?? event.text.slice(0, 100),
978
+ activityLog: [...prev.activityLog, newActivity]
979
+ };
980
+ }
981
+ case "message": {
982
+ return {
983
+ ...prev,
984
+ taskText: prev.taskText ?? event.text.slice(0, 100)
985
+ };
986
+ }
987
+ case "error": {
988
+ return {
989
+ ...prev,
990
+ error: new Error(event.message)
991
+ };
992
+ }
993
+ default:
994
+ return prev;
995
+ }
996
+ });
997
+ }, []);
998
+ useEffect2(() => {
999
+ mountedRef.current = true;
1000
+ startTimeRef.current = Date.now();
1001
+ toolIdRef.current = 0;
1002
+ activeToolsMapRef.current.clear();
1003
+ const harness = getHarness(harnessName);
1004
+ setState((prev) => ({ ...prev, isRunning: true, phase: "idle" }));
1005
+ const tickerInterval = setInterval(() => {
1006
+ if (mountedRef.current) {
1007
+ setState((prev) => ({
1008
+ ...prev,
1009
+ elapsedMs: Date.now() - startTimeRef.current
1010
+ }));
1011
+ }
1012
+ }, 1000);
1013
+ harness.run(prompt, {
1014
+ cwd: cwd ?? process.cwd(),
1015
+ model
1016
+ }, handleEvent).then((result) => {
1017
+ if (!mountedRef.current)
1018
+ return;
1019
+ setState((prev) => ({
1020
+ ...prev,
1021
+ phase: "done",
1022
+ isRunning: false,
1023
+ elapsedMs: result.durationMs,
1024
+ result: {
1025
+ totalCostUsd: result.costUsd,
1026
+ usage: result.usage
1027
+ },
1028
+ error: result.error ? new Error(result.error) : prev.error
1029
+ }));
1030
+ }).catch((err) => {
1031
+ if (!mountedRef.current)
1032
+ return;
1033
+ setState((prev) => ({
1034
+ ...prev,
1035
+ phase: "done",
1036
+ isRunning: false,
1037
+ error: err instanceof Error ? err : new Error(String(err))
1038
+ }));
1039
+ });
1040
+ return () => {
1041
+ mountedRef.current = false;
1042
+ clearInterval(tickerInterval);
1043
+ };
1044
+ }, [prompt, cwd, harnessName, model, handleEvent]);
1045
+ return state;
1046
+ }
1047
+
1048
+ // src/App.tsx
1049
+ import { join as join2 } from "path";
1050
+
1051
+ // src/lib/spec-parser.ts
1052
+ import { readFileSync, existsSync } from "fs";
1053
+ import { join } from "path";
1054
+ function parseSpec(specPath) {
1055
+ if (!existsSync(specPath)) {
1056
+ return null;
1057
+ }
1058
+ const content = readFileSync(specPath, "utf-8");
1059
+ return parseSpecContent(content);
1060
+ }
1061
+ function parseSpecContent(content) {
1062
+ const tasks = [];
1063
+ const lines = content.split(`
1064
+ `);
1065
+ let currentPhaseNumber = 0;
1066
+ let currentPhaseName = "Tasks";
1067
+ let taskCounter = 0;
1068
+ for (const line of lines) {
1069
+ const phaseMatch = line.match(/^#{2,3}\s+Phase\s*(\d+)\s*[:\-]\s*(.+)$/i);
1070
+ if (phaseMatch) {
1071
+ currentPhaseNumber = parseInt(phaseMatch[1], 10);
1072
+ currentPhaseName = phaseMatch[2].trim();
1073
+ continue;
1074
+ }
1075
+ const sectionMatch = line.match(/^#{2,3}\s+(.+)$/);
1076
+ if (sectionMatch) {
1077
+ const sectionTitle = sectionMatch[1].trim();
1078
+ if (!sectionTitle.toLowerCase().includes("summary")) {
1079
+ currentPhaseNumber++;
1080
+ currentPhaseName = sectionTitle;
1081
+ }
1082
+ continue;
1083
+ }
1084
+ const checkboxMatch = line.match(/^-\s*\[\s*\]\s+(.+)$/);
1085
+ if (checkboxMatch) {
1086
+ taskCounter++;
1087
+ const fullTaskText = checkboxMatch[1].trim();
1088
+ const taskNumMatch = fullTaskText.match(/^(\d+\.\d+)\s*:?\s*(.*)$/);
1089
+ let taskNumber;
1090
+ let taskText;
1091
+ if (taskNumMatch) {
1092
+ taskNumber = taskNumMatch[1];
1093
+ taskText = taskNumMatch[2] || fullTaskText;
1094
+ } else {
1095
+ taskNumber = `${currentPhaseNumber}.${taskCounter}`;
1096
+ taskText = fullTaskText;
1097
+ }
1098
+ tasks.push({
1099
+ taskNumber,
1100
+ phaseNumber: currentPhaseNumber,
1101
+ phaseName: currentPhaseName,
1102
+ taskText
1103
+ });
1104
+ }
1105
+ }
1106
+ if (tasks.length === 0) {
1107
+ return null;
1108
+ }
1109
+ return {
1110
+ totalIterations: tasks.length,
1111
+ tasks
1112
+ };
1113
+ }
1114
+ function getTaskForIteration(spec, iteration) {
1115
+ const index = iteration - 1;
1116
+ if (index < 0 || index >= spec.tasks.length) {
1117
+ return null;
1118
+ }
1119
+ return spec.tasks[index];
1120
+ }
1121
+ function loadSpecFromDir(dir) {
1122
+ const specPath = join(dir, "SPEC.md");
1123
+ return parseSpec(specPath);
1124
+ }
1125
+ function parseSpecTitle(content) {
1126
+ const lines = content.split(`
1127
+ `);
1128
+ for (const line of lines) {
1129
+ const match = line.match(/^#\s+(.+)$/);
1130
+ if (match) {
1131
+ return match[1].trim();
1132
+ }
1133
+ }
1134
+ return null;
1135
+ }
1136
+ function getSpecTitle(specPath) {
1137
+ if (!existsSync(specPath)) {
1138
+ return null;
1139
+ }
1140
+ const content = readFileSync(specPath, "utf-8");
1141
+ return parseSpecTitle(content);
1142
+ }
1143
+ function isSpecComplete(specPath) {
1144
+ if (!existsSync(specPath)) {
1145
+ return false;
1146
+ }
1147
+ const content = readFileSync(specPath, "utf-8");
1148
+ const hasUncheckedTasks = /^-\s*\[\s*\]\s+/m.test(content);
1149
+ return !hasUncheckedTasks;
1150
+ }
1151
+
1152
+ // src/App.tsx
1153
+ import { jsxDEV as jsxDEV11, Fragment } from "react/jsx-dev-runtime";
1154
+ function buildFailureContext(toolGroups, activityLog) {
1155
+ const allTools = toolGroups.flatMap((g) => g.tools);
1156
+ const lastTool = allTools[allTools.length - 1];
1157
+ const errorTool = allTools.find((t) => t.isError) ?? lastTool;
1158
+ const recentActivity = activityLog.slice(-5).map((item) => {
1159
+ if (item.type === "thought")
1160
+ return `\uD83D\uDCAD ${item.text.slice(0, 100)}`;
1161
+ if (item.type === "tool_start")
1162
+ return `▶ ${item.displayName}`;
1163
+ if (item.type === "tool_complete") {
1164
+ const icon = item.isError ? "✗" : "✓";
1165
+ return `${icon} ${item.displayName} (${(item.durationMs / 1000).toFixed(1)}s)`;
1166
+ }
1167
+ if (item.type === "commit")
1168
+ return `\uD83D\uDCDD ${item.hash.slice(0, 7)} ${item.message}`;
1169
+ return "";
1170
+ }).filter(Boolean);
1171
+ return {
1172
+ lastToolName: errorTool?.name ?? null,
1173
+ lastToolInput: errorTool?.input ? formatToolInput(errorTool.input) : null,
1174
+ lastToolOutput: errorTool?.output?.slice(0, 500) ?? null,
1175
+ recentActivity
1176
+ };
1177
+ }
1178
+ function formatToolInput(input) {
1179
+ if (input.command)
1180
+ return `command: ${String(input.command).slice(0, 200)}`;
1181
+ if (input.file_path)
1182
+ return `file: ${String(input.file_path)}`;
1183
+ if (input.pattern)
1184
+ return `pattern: ${String(input.pattern)}`;
1185
+ if (input.prompt)
1186
+ return `prompt: ${String(input.prompt).slice(0, 100)}`;
1187
+ return JSON.stringify(input).slice(0, 200);
1188
+ }
1189
+ function AppInner({
1190
+ state,
1191
+ iteration,
1192
+ totalIterations,
1193
+ specTaskText,
1194
+ taskNumber,
1195
+ phaseName,
1196
+ completedResults,
1197
+ onIterationComplete
1198
+ }) {
1199
+ const elapsedSeconds = Math.floor(state.elapsedMs / 1000);
1200
+ useEffect3(() => {
1201
+ if (state.phase === "done" && !state.isRunning && onIterationComplete) {
1202
+ onIterationComplete({
1203
+ iteration,
1204
+ durationMs: state.elapsedMs,
1205
+ stats: state.stats,
1206
+ error: state.error,
1207
+ taskText: state.taskText,
1208
+ specTaskText,
1209
+ lastCommit: state.lastCommit,
1210
+ costUsd: state.result?.totalCostUsd ?? null,
1211
+ usage: state.result?.usage ?? null,
1212
+ taskNumber,
1213
+ phaseName,
1214
+ failureContext: state.error ? buildFailureContext(state.toolGroups, state.activityLog) : null
1215
+ });
1216
+ }
1217
+ }, [state.phase, state.isRunning, iteration, state.elapsedMs, state.stats, state.error, state.taskText, specTaskText, state.lastCommit, state.result, onIterationComplete, taskNumber, phaseName, state.toolGroups, state.activityLog]);
1218
+ const isPending = state.phase === "idle" || !state.taskText;
1219
+ return /* @__PURE__ */ jsxDEV11(Box11, {
1220
+ flexDirection: "column",
1221
+ children: [
1222
+ /* @__PURE__ */ jsxDEV11(CompletedIterationsList, {
1223
+ results: completedResults
1224
+ }, undefined, false, undefined, this),
1225
+ /* @__PURE__ */ jsxDEV11(IterationHeader, {
1226
+ current: iteration,
1227
+ total: totalIterations,
1228
+ elapsedSeconds
1229
+ }, undefined, false, undefined, this),
1230
+ /* @__PURE__ */ jsxDEV11(TaskTitle, {
1231
+ text: state.taskText ?? undefined,
1232
+ isPending
1233
+ }, undefined, false, undefined, this),
1234
+ /* @__PURE__ */ jsxDEV11(PhaseIndicator, {
1235
+ phase: state.phase
1236
+ }, undefined, false, undefined, this),
1237
+ /* @__PURE__ */ jsxDEV11(ActivityFeed, {
1238
+ activityLog: state.activityLog
1239
+ }, undefined, false, undefined, this),
1240
+ state.error && /* @__PURE__ */ jsxDEV11(Box11, {
1241
+ marginLeft: 2,
1242
+ children: /* @__PURE__ */ jsxDEV11(StatusMessage, {
1243
+ variant: "error",
1244
+ children: state.error.message
1245
+ }, undefined, false, undefined, this)
1246
+ }, undefined, false, undefined, this),
1247
+ /* @__PURE__ */ jsxDEV11(Box11, {
1248
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
1249
+ color: "cyan",
1250
+ children: "│"
1251
+ }, undefined, false, undefined, this)
1252
+ }, undefined, false, undefined, this),
1253
+ /* @__PURE__ */ jsxDEV11(StatusBar, {
1254
+ phase: state.phase,
1255
+ elapsedSeconds,
1256
+ lastCommit: state.lastCommit ?? undefined
1257
+ }, undefined, false, undefined, this)
1258
+ ]
1259
+ }, undefined, true, undefined, this);
1260
+ }
1261
+ function App({
1262
+ prompt,
1263
+ iteration = 1,
1264
+ totalIterations = 1,
1265
+ cwd,
1266
+ model,
1267
+ harness = "claude",
1268
+ _mockState,
1269
+ onIterationComplete,
1270
+ completedResults = [],
1271
+ taskNumber = null,
1272
+ phaseName = null,
1273
+ specTaskText = null
1274
+ }) {
1275
+ const liveState = useHarnessStream({
1276
+ prompt,
1277
+ cwd,
1278
+ harness,
1279
+ model,
1280
+ iteration,
1281
+ totalIterations
1282
+ });
1283
+ const state = _mockState ?? liveState;
1284
+ return /* @__PURE__ */ jsxDEV11(AppInner, {
1285
+ state,
1286
+ iteration,
1287
+ totalIterations,
1288
+ specTaskText,
1289
+ taskNumber,
1290
+ phaseName,
1291
+ completedResults,
1292
+ onIterationComplete
1293
+ }, undefined, false, undefined, this);
1294
+ }
1295
+ function formatDuration3(ms) {
1296
+ const seconds = Math.floor(ms / 1000);
1297
+ if (seconds < 60) {
1298
+ return `${seconds}s`;
1299
+ }
1300
+ const minutes = Math.floor(seconds / 60);
1301
+ const remainingSeconds = seconds % 60;
1302
+ if (minutes < 60) {
1303
+ return `${minutes}m ${remainingSeconds}s`;
1304
+ }
1305
+ const hours = Math.floor(minutes / 60);
1306
+ const remainingMinutes = minutes % 60;
1307
+ return `${hours}h ${remainingMinutes}m`;
1308
+ }
1309
+ function aggregateStats(results) {
1310
+ return results.reduce((acc, result) => ({
1311
+ toolsStarted: acc.toolsStarted + result.stats.toolsStarted,
1312
+ toolsCompleted: acc.toolsCompleted + result.stats.toolsCompleted,
1313
+ toolsErrored: acc.toolsErrored + result.stats.toolsErrored,
1314
+ reads: acc.reads + result.stats.reads,
1315
+ writes: acc.writes + result.stats.writes,
1316
+ commands: acc.commands + result.stats.commands,
1317
+ metaOps: acc.metaOps + result.stats.metaOps
1318
+ }), {
1319
+ toolsStarted: 0,
1320
+ toolsCompleted: 0,
1321
+ toolsErrored: 0,
1322
+ reads: 0,
1323
+ writes: 0,
1324
+ commands: 0,
1325
+ metaOps: 0
1326
+ });
1327
+ }
1328
+ function IterationRunner({
1329
+ prompt,
1330
+ totalIterations,
1331
+ cwd,
1332
+ idleTimeoutMs,
1333
+ saveJsonl,
1334
+ model,
1335
+ harness,
1336
+ _mockResults,
1337
+ _mockCurrentIteration,
1338
+ _mockIsComplete,
1339
+ _mockState
1340
+ }) {
1341
+ const { exit } = useApp();
1342
+ const [currentIteration, setCurrentIteration] = useState3(_mockCurrentIteration ?? 1);
1343
+ const [results, setResults] = useState3(_mockResults ?? []);
1344
+ const [isComplete, setIsComplete] = useState3(_mockIsComplete ?? false);
1345
+ const [iterationKey, setIterationKey] = useState3(0);
1346
+ const [spec, setSpec] = useState3(null);
1347
+ useEffect3(() => {
1348
+ const targetDir = cwd ?? process.cwd();
1349
+ const loadedSpec = loadSpecFromDir(targetDir);
1350
+ setSpec(loadedSpec);
1351
+ }, [cwd]);
1352
+ const handleIterationComplete = useCallback2((result) => {
1353
+ setResults((prev) => [...prev, result]);
1354
+ if (result.error) {
1355
+ setIsComplete(true);
1356
+ return;
1357
+ }
1358
+ const targetDir = cwd ?? process.cwd();
1359
+ const specPath = join2(targetDir, "SPEC.md");
1360
+ if (isSpecComplete(specPath)) {
1361
+ setIsComplete(true);
1362
+ return;
1363
+ }
1364
+ const updatedSpec = loadSpecFromDir(targetDir);
1365
+ if (updatedSpec) {
1366
+ setSpec(updatedSpec);
1367
+ }
1368
+ if (currentIteration < totalIterations) {
1369
+ setCurrentIteration((prev) => prev + 1);
1370
+ setIterationKey((prev) => prev + 1);
1371
+ } else {
1372
+ setIsComplete(true);
1373
+ }
1374
+ }, [currentIteration, totalIterations, cwd]);
1375
+ useEffect3(() => {
1376
+ if (isComplete && !_mockIsComplete) {
1377
+ const timer = setTimeout(() => {
1378
+ exit();
1379
+ }, 100);
1380
+ return () => clearTimeout(timer);
1381
+ }
1382
+ }, [isComplete, exit, _mockIsComplete]);
1383
+ if (isComplete) {
1384
+ const totalDuration = results.reduce((acc, r) => acc + r.durationMs, 0);
1385
+ const successCount = results.filter((r) => !r.error).length;
1386
+ const errorCount = results.filter((r) => r.error).length;
1387
+ const stats = aggregateStats(results);
1388
+ return /* @__PURE__ */ jsxDEV11(Box11, {
1389
+ flexDirection: "column",
1390
+ children: [
1391
+ /* @__PURE__ */ jsxDEV11(Box11, {
1392
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
1393
+ color: "cyan",
1394
+ children: "╔═══════════════════════════════════════════════════════╗"
1395
+ }, undefined, false, undefined, this)
1396
+ }, undefined, false, undefined, this),
1397
+ /* @__PURE__ */ jsxDEV11(Box11, {
1398
+ children: [
1399
+ /* @__PURE__ */ jsxDEV11(Text10, {
1400
+ color: "cyan",
1401
+ children: "║"
1402
+ }, undefined, false, undefined, this),
1403
+ /* @__PURE__ */ jsxDEV11(Text10, {
1404
+ color: "green",
1405
+ bold: true,
1406
+ children: " ✓ All iterations complete"
1407
+ }, undefined, false, undefined, this),
1408
+ /* @__PURE__ */ jsxDEV11(Text10, {
1409
+ color: "cyan",
1410
+ children: " ║"
1411
+ }, undefined, false, undefined, this)
1412
+ ]
1413
+ }, undefined, true, undefined, this),
1414
+ /* @__PURE__ */ jsxDEV11(Box11, {
1415
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
1416
+ color: "cyan",
1417
+ children: "╠═══════════════════════════════════════════════════════╣"
1418
+ }, undefined, false, undefined, this)
1419
+ }, undefined, false, undefined, this),
1420
+ /* @__PURE__ */ jsxDEV11(Box11, {
1421
+ children: [
1422
+ /* @__PURE__ */ jsxDEV11(Text10, {
1423
+ color: "cyan",
1424
+ children: "║"
1425
+ }, undefined, false, undefined, this),
1426
+ /* @__PURE__ */ jsxDEV11(Text10, {
1427
+ children: " Iterations: "
1428
+ }, undefined, false, undefined, this),
1429
+ /* @__PURE__ */ jsxDEV11(Text10, {
1430
+ color: "green",
1431
+ children: [
1432
+ successCount,
1433
+ " succeeded"
1434
+ ]
1435
+ }, undefined, true, undefined, this),
1436
+ errorCount > 0 && /* @__PURE__ */ jsxDEV11(Fragment, {
1437
+ children: [
1438
+ /* @__PURE__ */ jsxDEV11(Text10, {
1439
+ children: ", "
1440
+ }, undefined, false, undefined, this),
1441
+ /* @__PURE__ */ jsxDEV11(Text10, {
1442
+ color: "red",
1443
+ children: [
1444
+ errorCount,
1445
+ " failed"
1446
+ ]
1447
+ }, undefined, true, undefined, this)
1448
+ ]
1449
+ }, undefined, true, undefined, this),
1450
+ /* @__PURE__ */ jsxDEV11(Text10, {
1451
+ color: "cyan",
1452
+ children: " ║"
1453
+ }, undefined, false, undefined, this)
1454
+ ]
1455
+ }, undefined, true, undefined, this),
1456
+ /* @__PURE__ */ jsxDEV11(Box11, {
1457
+ children: [
1458
+ /* @__PURE__ */ jsxDEV11(Text10, {
1459
+ color: "cyan",
1460
+ children: "║"
1461
+ }, undefined, false, undefined, this),
1462
+ /* @__PURE__ */ jsxDEV11(Text10, {
1463
+ children: " Duration: "
1464
+ }, undefined, false, undefined, this),
1465
+ /* @__PURE__ */ jsxDEV11(Text10, {
1466
+ color: "yellow",
1467
+ children: formatDuration3(totalDuration)
1468
+ }, undefined, false, undefined, this),
1469
+ /* @__PURE__ */ jsxDEV11(Text10, {
1470
+ color: "cyan",
1471
+ children: " ║"
1472
+ }, undefined, false, undefined, this)
1473
+ ]
1474
+ }, undefined, true, undefined, this),
1475
+ /* @__PURE__ */ jsxDEV11(Box11, {
1476
+ children: [
1477
+ /* @__PURE__ */ jsxDEV11(Text10, {
1478
+ color: "cyan",
1479
+ children: "║"
1480
+ }, undefined, false, undefined, this),
1481
+ /* @__PURE__ */ jsxDEV11(Text10, {
1482
+ children: " Tools: "
1483
+ }, undefined, false, undefined, this),
1484
+ /* @__PURE__ */ jsxDEV11(Text10, {
1485
+ children: [
1486
+ stats.reads,
1487
+ " reads, ",
1488
+ stats.writes,
1489
+ " writes, ",
1490
+ stats.commands,
1491
+ " commands"
1492
+ ]
1493
+ }, undefined, true, undefined, this),
1494
+ /* @__PURE__ */ jsxDEV11(Text10, {
1495
+ color: "cyan",
1496
+ children: " ║"
1497
+ }, undefined, false, undefined, this)
1498
+ ]
1499
+ }, undefined, true, undefined, this),
1500
+ /* @__PURE__ */ jsxDEV11(Box11, {
1501
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
1502
+ color: "cyan",
1503
+ children: "╚═══════════════════════════════════════════════════════╝"
1504
+ }, undefined, false, undefined, this)
1505
+ }, undefined, false, undefined, this),
1506
+ results.map((result, idx) => {
1507
+ const displayText = result.specTaskText ?? result.taskText ?? "Unknown task";
1508
+ const truncatedText = displayText.length > 40 ? displayText.slice(0, 40) + "..." : displayText;
1509
+ return /* @__PURE__ */ jsxDEV11(Box11, {
1510
+ flexDirection: "column",
1511
+ children: [
1512
+ /* @__PURE__ */ jsxDEV11(Box11, {
1513
+ children: [
1514
+ /* @__PURE__ */ jsxDEV11(Text10, {
1515
+ color: result.error ? "red" : "green",
1516
+ children: result.error ? "✗" : "✓"
1517
+ }, undefined, false, undefined, this),
1518
+ /* @__PURE__ */ jsxDEV11(Text10, {
1519
+ color: "cyan",
1520
+ children: [
1521
+ " ",
1522
+ result.taskNumber ?? result.iteration,
1523
+ ". "
1524
+ ]
1525
+ }, undefined, true, undefined, this),
1526
+ result.phaseName && /* @__PURE__ */ jsxDEV11(Text10, {
1527
+ color: "yellow",
1528
+ children: [
1529
+ "[",
1530
+ result.phaseName,
1531
+ "] "
1532
+ ]
1533
+ }, undefined, true, undefined, this),
1534
+ /* @__PURE__ */ jsxDEV11(Text10, {
1535
+ color: "gray",
1536
+ children: truncatedText
1537
+ }, undefined, false, undefined, this),
1538
+ /* @__PURE__ */ jsxDEV11(Text10, {
1539
+ color: "gray",
1540
+ children: [
1541
+ " (",
1542
+ formatDuration3(result.durationMs),
1543
+ ")"
1544
+ ]
1545
+ }, undefined, true, undefined, this)
1546
+ ]
1547
+ }, undefined, true, undefined, this),
1548
+ result.error && /* @__PURE__ */ jsxDEV11(Box11, {
1549
+ flexDirection: "column",
1550
+ marginLeft: 2,
1551
+ children: [
1552
+ /* @__PURE__ */ jsxDEV11(Box11, {
1553
+ children: [
1554
+ /* @__PURE__ */ jsxDEV11(Text10, {
1555
+ color: "red",
1556
+ children: " Error: "
1557
+ }, undefined, false, undefined, this),
1558
+ /* @__PURE__ */ jsxDEV11(Text10, {
1559
+ color: "gray",
1560
+ children: [
1561
+ result.error.message.slice(0, 80),
1562
+ result.error.message.length > 80 ? "..." : ""
1563
+ ]
1564
+ }, undefined, true, undefined, this)
1565
+ ]
1566
+ }, undefined, true, undefined, this),
1567
+ result.failureContext?.lastToolName && /* @__PURE__ */ jsxDEV11(Box11, {
1568
+ children: [
1569
+ /* @__PURE__ */ jsxDEV11(Text10, {
1570
+ color: "yellow",
1571
+ children: " Tool: "
1572
+ }, undefined, false, undefined, this),
1573
+ /* @__PURE__ */ jsxDEV11(Text10, {
1574
+ color: "gray",
1575
+ children: result.failureContext.lastToolName
1576
+ }, undefined, false, undefined, this)
1577
+ ]
1578
+ }, undefined, true, undefined, this),
1579
+ result.failureContext?.lastToolInput && /* @__PURE__ */ jsxDEV11(Box11, {
1580
+ children: [
1581
+ /* @__PURE__ */ jsxDEV11(Text10, {
1582
+ color: "yellow",
1583
+ children: " Input: "
1584
+ }, undefined, false, undefined, this),
1585
+ /* @__PURE__ */ jsxDEV11(Text10, {
1586
+ color: "gray",
1587
+ children: [
1588
+ result.failureContext.lastToolInput.slice(0, 100),
1589
+ result.failureContext.lastToolInput.length > 100 ? "..." : ""
1590
+ ]
1591
+ }, undefined, true, undefined, this)
1592
+ ]
1593
+ }, undefined, true, undefined, this),
1594
+ result.failureContext?.lastToolOutput && /* @__PURE__ */ jsxDEV11(Box11, {
1595
+ children: [
1596
+ /* @__PURE__ */ jsxDEV11(Text10, {
1597
+ color: "yellow",
1598
+ children: " Output: "
1599
+ }, undefined, false, undefined, this),
1600
+ /* @__PURE__ */ jsxDEV11(Text10, {
1601
+ color: "gray",
1602
+ children: [
1603
+ result.failureContext.lastToolOutput.slice(0, 150),
1604
+ result.failureContext.lastToolOutput.length > 150 ? "..." : ""
1605
+ ]
1606
+ }, undefined, true, undefined, this)
1607
+ ]
1608
+ }, undefined, true, undefined, this),
1609
+ result.failureContext?.recentActivity && result.failureContext.recentActivity.length > 0 && /* @__PURE__ */ jsxDEV11(Box11, {
1610
+ flexDirection: "column",
1611
+ children: [
1612
+ /* @__PURE__ */ jsxDEV11(Text10, {
1613
+ color: "yellow",
1614
+ children: " Recent activity:"
1615
+ }, undefined, false, undefined, this),
1616
+ result.failureContext.recentActivity.map((activity, i) => /* @__PURE__ */ jsxDEV11(Box11, {
1617
+ marginLeft: 2,
1618
+ children: /* @__PURE__ */ jsxDEV11(Text10, {
1619
+ color: "gray",
1620
+ children: activity.slice(0, 80)
1621
+ }, undefined, false, undefined, this)
1622
+ }, i, false, undefined, this))
1623
+ ]
1624
+ }, undefined, true, undefined, this)
1625
+ ]
1626
+ }, undefined, true, undefined, this),
1627
+ result.lastCommit && /* @__PURE__ */ jsxDEV11(Box11, {
1628
+ marginLeft: 2,
1629
+ children: [
1630
+ /* @__PURE__ */ jsxDEV11(Text10, {
1631
+ color: "green",
1632
+ children: "✓ "
1633
+ }, undefined, false, undefined, this),
1634
+ /* @__PURE__ */ jsxDEV11(Text10, {
1635
+ color: "yellow",
1636
+ children: result.lastCommit.hash.slice(0, 7)
1637
+ }, undefined, false, undefined, this),
1638
+ /* @__PURE__ */ jsxDEV11(Text10, {
1639
+ color: "gray",
1640
+ children: [
1641
+ " - ",
1642
+ result.lastCommit.message.slice(0, 50),
1643
+ result.lastCommit.message.length > 50 ? "..." : ""
1644
+ ]
1645
+ }, undefined, true, undefined, this)
1646
+ ]
1647
+ }, undefined, true, undefined, this)
1648
+ ]
1649
+ }, idx, true, undefined, this);
1650
+ })
1651
+ ]
1652
+ }, undefined, true, undefined, this);
1653
+ }
1654
+ const currentTask = spec ? getTaskForIteration(spec, currentIteration) : null;
1655
+ return /* @__PURE__ */ jsxDEV11(App, {
1656
+ prompt,
1657
+ iteration: currentIteration,
1658
+ totalIterations,
1659
+ cwd,
1660
+ idleTimeoutMs,
1661
+ saveJsonl,
1662
+ model,
1663
+ harness,
1664
+ onIterationComplete: handleIterationComplete,
1665
+ _mockState,
1666
+ completedResults: results,
1667
+ taskNumber: currentTask?.taskNumber ?? null,
1668
+ phaseName: currentTask?.phaseName ?? null,
1669
+ specTaskText: currentTask?.taskText ?? null
1670
+ }, iterationKey, false, undefined, this);
1671
+ }
1672
+
1673
+ // src/commands/init.ts
1674
+ import { existsSync as existsSync2, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
1675
+ import { join as join3, dirname } from "path";
1676
+ import { fileURLToPath } from "url";
1677
+ var __filename2 = fileURLToPath(import.meta.url);
1678
+ var __dirname2 = dirname(__filename2);
1679
+ function getTemplatesDir() {
1680
+ return join3(__dirname2, "..", "..", "templates");
1681
+ }
1682
+ function copyRecursive(src, dest, created, skipped) {
1683
+ const entries = readdirSync(src);
1684
+ for (const entry of entries) {
1685
+ const srcPath = join3(src, entry);
1686
+ const destPath = join3(dest, entry);
1687
+ const stat = statSync(srcPath);
1688
+ if (stat.isDirectory()) {
1689
+ if (!existsSync2(destPath)) {
1690
+ mkdirSync(destPath, { recursive: true });
1691
+ }
1692
+ copyRecursive(srcPath, destPath, created, skipped);
1693
+ } else {
1694
+ if (existsSync2(destPath)) {
1695
+ skipped.push(destPath);
1696
+ } else {
1697
+ mkdirSync(dirname(destPath), { recursive: true });
1698
+ copyFileSync(srcPath, destPath);
1699
+ created.push(destPath);
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ function runInit(targetDir) {
1705
+ const templatesDir = getTemplatesDir();
1706
+ if (!existsSync2(templatesDir)) {
1707
+ throw new Error(`Templates directory not found: ${templatesDir}`);
1708
+ }
1709
+ const created = [];
1710
+ const skipped = [];
1711
+ copyRecursive(templatesDir, targetDir, created, skipped);
1712
+ return { created, skipped };
1713
+ }
1714
+
1715
+ // src/commands/run.ts
1716
+ import { existsSync as existsSync3 } from "fs";
1717
+ import { join as join4 } from "path";
1718
+ import { execSync } from "child_process";
1719
+ function isGitRepo(cwd) {
1720
+ try {
1721
+ execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
1722
+ return true;
1723
+ } catch {
1724
+ return false;
1725
+ }
1726
+ }
1727
+ function hasUncommittedChanges(cwd) {
1728
+ try {
1729
+ const status = execSync("git status --porcelain", { cwd, encoding: "utf-8" });
1730
+ return status.trim().length > 0;
1731
+ } catch {
1732
+ return false;
1733
+ }
1734
+ }
1735
+ function validateProject(cwd) {
1736
+ const errors = [];
1737
+ const warnings = [];
1738
+ if (isGitRepo(cwd) && hasUncommittedChanges(cwd)) {
1739
+ errors.push("Uncommitted changes detected. Commit or stash before running Ralphie.");
1740
+ }
1741
+ const specPath = join4(cwd, "SPEC.md");
1742
+ if (!existsSync3(specPath)) {
1743
+ errors.push("SPEC.md not found. Create a SPEC.md with your project tasks.");
1744
+ }
1745
+ const ralphieMdPath = join4(cwd, ".claude", "ralphie.md");
1746
+ if (!existsSync3(ralphieMdPath)) {
1747
+ errors.push(".claude/ralphie.md not found. Run `ralphie init` first.");
1748
+ }
1749
+ const aiRalphiePath = join4(cwd, ".ai", "ralphie");
1750
+ if (!existsSync3(aiRalphiePath)) {
1751
+ errors.push(".ai/ralphie/ not found. Run `ralphie init` first.");
1752
+ }
1753
+ const valid = errors.length === 0;
1754
+ return { valid, errors, warnings };
1755
+ }
1756
+
1757
+ // src/commands/upgrade.ts
1758
+ import { existsSync as existsSync4, renameSync, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
1759
+ import { join as join5, dirname as dirname2 } from "path";
1760
+ import { fileURLToPath as fileURLToPath2 } from "url";
1761
+ var __filename3 = fileURLToPath2(import.meta.url);
1762
+ var __dirname3 = dirname2(__filename3);
1763
+ function getTemplatesDir2() {
1764
+ return join5(__dirname3, "..", "..", "templates");
1765
+ }
1766
+ var CURRENT_VERSION = 2;
1767
+ var VERSION_DEFINITIONS = [
1768
+ {
1769
+ version: 1,
1770
+ name: "v1 (PRD/progress.txt)",
1771
+ indicators: ["PRD.md", "PRD", "progress.txt"]
1772
+ },
1773
+ {
1774
+ version: 2,
1775
+ name: "v2 (SPEC.md/STATE.txt)",
1776
+ indicators: ["SPEC.md", "STATE.txt"]
1777
+ }
1778
+ ];
1779
+ function detectVersion(targetDir) {
1780
+ const foundIndicators = [];
1781
+ const legacyFiles = [];
1782
+ let detectedVersion = null;
1783
+ for (const versionDef of [...VERSION_DEFINITIONS].reverse()) {
1784
+ const found = versionDef.indicators.filter((indicator) => existsSync4(join5(targetDir, indicator)));
1785
+ if (found.length > 0) {
1786
+ if (detectedVersion === null || versionDef.version > detectedVersion) {
1787
+ detectedVersion = versionDef.version;
1788
+ }
1789
+ foundIndicators.push(...found);
1790
+ if (versionDef.version < CURRENT_VERSION) {
1791
+ legacyFiles.push(...found);
1792
+ }
1793
+ }
1794
+ }
1795
+ return {
1796
+ detectedVersion,
1797
+ foundIndicators: [...new Set(foundIndicators)],
1798
+ isLatest: detectedVersion === CURRENT_VERSION,
1799
+ hasLegacyFiles: legacyFiles.length > 0,
1800
+ legacyFiles: [...new Set(legacyFiles)]
1801
+ };
1802
+ }
1803
+ var migrations = {
1804
+ "1->2": migrateV1ToV2
1805
+ };
1806
+ function migrateV1ToV2(targetDir, result) {
1807
+ const prdPath = join5(targetDir, "PRD.md");
1808
+ const prdAltPath = join5(targetDir, "PRD");
1809
+ const specPath = join5(targetDir, "SPEC.md");
1810
+ if (existsSync4(prdPath)) {
1811
+ if (existsSync4(specPath)) {
1812
+ result.warnings.push("Both PRD.md and SPEC.md exist - keeping both, please merge manually");
1813
+ } else {
1814
+ renameSync(prdPath, specPath);
1815
+ result.renamed.push({ from: "PRD.md", to: "SPEC.md" });
1816
+ }
1817
+ } else if (existsSync4(prdAltPath)) {
1818
+ if (existsSync4(specPath)) {
1819
+ result.warnings.push("Both PRD and SPEC.md exist - keeping both, please merge manually");
1820
+ } else {
1821
+ renameSync(prdAltPath, specPath);
1822
+ result.renamed.push({ from: "PRD", to: "SPEC.md" });
1823
+ }
1824
+ }
1825
+ const progressPath = join5(targetDir, "progress.txt");
1826
+ const statePath = join5(targetDir, "STATE.txt");
1827
+ if (existsSync4(progressPath)) {
1828
+ if (existsSync4(statePath)) {
1829
+ result.warnings.push("Both progress.txt and STATE.txt exist - keeping both, please merge manually");
1830
+ } else {
1831
+ const progressContent = readFileSync2(progressPath, "utf-8");
1832
+ const newContent = `# Progress Log
1833
+
1834
+ ${progressContent}`;
1835
+ writeFileSync(statePath, newContent, "utf-8");
1836
+ renameSync(progressPath, join5(targetDir, "progress.txt.bak"));
1837
+ result.renamed.push({ from: "progress.txt", to: "STATE.txt" });
1838
+ }
1839
+ } else if (!existsSync4(statePath)) {
1840
+ writeFileSync(statePath, `# Progress Log
1841
+
1842
+ `, "utf-8");
1843
+ result.created.push("STATE.txt");
1844
+ }
1845
+ ensureV2Structure(targetDir, result);
1846
+ }
1847
+ function ensureV2Structure(targetDir, result) {
1848
+ const aiRalphieDir = join5(targetDir, ".ai", "ralphie");
1849
+ if (!existsSync4(aiRalphieDir)) {
1850
+ mkdirSync2(aiRalphieDir, { recursive: true });
1851
+ result.created.push(".ai/ralphie/");
1852
+ }
1853
+ const gitkeepPath = join5(aiRalphieDir, ".gitkeep");
1854
+ if (!existsSync4(gitkeepPath)) {
1855
+ writeFileSync(gitkeepPath, "", "utf-8");
1856
+ result.created.push(".ai/ralphie/.gitkeep");
1857
+ }
1858
+ const claudeDir = join5(targetDir, ".claude");
1859
+ if (!existsSync4(claudeDir)) {
1860
+ mkdirSync2(claudeDir, { recursive: true });
1861
+ }
1862
+ const ralphieMdDest = join5(claudeDir, "ralphie.md");
1863
+ const templatesDir = getTemplatesDir2();
1864
+ const ralphieMdSrc = join5(templatesDir, ".claude", "ralphie.md");
1865
+ if (!existsSync4(ralphieMdDest)) {
1866
+ if (existsSync4(ralphieMdSrc)) {
1867
+ copyFileSync2(ralphieMdSrc, ralphieMdDest);
1868
+ result.created.push(".claude/ralphie.md");
1869
+ }
1870
+ } else {
1871
+ const existingContent = readFileSync2(ralphieMdDest, "utf-8");
1872
+ const hasOldPatterns = /\bPRD\b/.test(existingContent) || /\bprogress\.txt\b/.test(existingContent);
1873
+ if (hasOldPatterns) {
1874
+ if (existsSync4(ralphieMdSrc)) {
1875
+ copyFileSync2(ralphieMdSrc, ralphieMdDest);
1876
+ result.renamed.push({ from: ".claude/ralphie.md (old)", to: ".claude/ralphie.md (v2)" });
1877
+ }
1878
+ } else {
1879
+ result.skipped.push(".claude/ralphie.md (already v2)");
1880
+ }
1881
+ }
1882
+ const claudeMdDest = join5(claudeDir, "CLAUDE.md");
1883
+ if (existsSync4(claudeMdDest)) {
1884
+ const existingContent = readFileSync2(claudeMdDest, "utf-8");
1885
+ const hasOldPatterns = /\bPRD\b/.test(existingContent) || /\bprogress\.txt\b/.test(existingContent);
1886
+ if (hasOldPatterns) {
1887
+ result.warnings.push(".claude/CLAUDE.md contains old patterns (PRD/progress.txt) - update manually");
1888
+ }
1889
+ }
1890
+ }
1891
+ function getMigrationPath(fromVersion, toVersion) {
1892
+ const path = [];
1893
+ let current = fromVersion;
1894
+ while (current < toVersion) {
1895
+ const next = current + 1;
1896
+ const key = `${current}->${next}`;
1897
+ if (!migrations[key]) {
1898
+ throw new Error(`No migration path from v${current} to v${next}`);
1899
+ }
1900
+ path.push(key);
1901
+ current = next;
1902
+ }
1903
+ return path;
1904
+ }
1905
+ function runUpgrade(targetDir, targetVersion = CURRENT_VERSION) {
1906
+ const detection = detectVersion(targetDir);
1907
+ if (detection.detectedVersion === null) {
1908
+ throw new Error("Could not detect project version. Is this a Ralphie project?");
1909
+ }
1910
+ if (detection.detectedVersion >= targetVersion) {
1911
+ throw new Error(`Project is already at v${detection.detectedVersion} (target: v${targetVersion})`);
1912
+ }
1913
+ const result = {
1914
+ fromVersion: detection.detectedVersion,
1915
+ toVersion: targetVersion,
1916
+ renamed: [],
1917
+ created: [],
1918
+ skipped: [],
1919
+ warnings: []
1920
+ };
1921
+ const migrationPath = getMigrationPath(detection.detectedVersion, targetVersion);
1922
+ for (const step of migrationPath) {
1923
+ const migrationFn = migrations[step];
1924
+ migrationFn(targetDir, result);
1925
+ }
1926
+ return result;
1927
+ }
1928
+ function getVersionName(version) {
1929
+ const def = VERSION_DEFINITIONS.find((v) => v.version === version);
1930
+ return def?.name ?? `v${version}`;
1931
+ }
1932
+
1933
+ // src/lib/git.ts
1934
+ import { execSync as execSync2 } from "child_process";
1935
+ function getCurrentBranch(cwd) {
1936
+ try {
1937
+ return execSync2("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8" }).trim();
1938
+ } catch {
1939
+ return null;
1940
+ }
1941
+ }
1942
+ function isOnMainBranch(cwd) {
1943
+ const branch = getCurrentBranch(cwd);
1944
+ return branch === "main" || branch === "master";
1945
+ }
1946
+ function branchExists(cwd, branchName) {
1947
+ try {
1948
+ execSync2(`git rev-parse --verify ${branchName}`, { cwd, stdio: "pipe" });
1949
+ return true;
1950
+ } catch {
1951
+ return false;
1952
+ }
1953
+ }
1954
+ function slugifyTitle(title) {
1955
+ return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
1956
+ }
1957
+ function createFeatureBranch(cwd, title) {
1958
+ if (!isOnMainBranch(cwd)) {
1959
+ return { created: false, branchName: null, error: null };
1960
+ }
1961
+ const slug = slugifyTitle(title);
1962
+ if (!slug) {
1963
+ return { created: false, branchName: null, error: "Could not generate branch name from title" };
1964
+ }
1965
+ const branchName = `feat/${slug}`;
1966
+ if (branchExists(cwd, branchName)) {
1967
+ try {
1968
+ execSync2(`git checkout ${branchName}`, { cwd, stdio: "pipe" });
1969
+ return { created: false, branchName, error: null };
1970
+ } catch {
1971
+ return { created: false, branchName: null, error: `Failed to checkout ${branchName}` };
1972
+ }
1973
+ }
1974
+ try {
1975
+ execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
1976
+ return { created: true, branchName, error: null };
1977
+ } catch {
1978
+ return { created: false, branchName: null, error: `Failed to create branch ${branchName}` };
1979
+ }
1980
+ }
1981
+
1982
+ // src/lib/headless-emitter.ts
1983
+ function emit(event) {
1984
+ console.log(JSON.stringify(event));
1985
+ }
1986
+ function emitStarted(spec, tasks, model, harness) {
1987
+ emit({
1988
+ event: "started",
1989
+ spec,
1990
+ tasks,
1991
+ model,
1992
+ harness,
1993
+ timestamp: new Date().toISOString()
1994
+ });
1995
+ }
1996
+ function emitIteration(n, phase) {
1997
+ emit({ event: "iteration", n, phase });
1998
+ }
1999
+ function emitTool(type, path) {
2000
+ emit({ event: "tool", type, path });
2001
+ }
2002
+ function emitCommit(hash, message) {
2003
+ emit({ event: "commit", hash, message });
2004
+ }
2005
+ function emitTaskComplete(index, text) {
2006
+ emit({ event: "task_complete", index, text });
2007
+ }
2008
+ function emitIterationDone(n, duration_ms, stats) {
2009
+ emit({ event: "iteration_done", n, duration_ms, stats });
2010
+ }
2011
+ function emitStuck(reason, iterations_without_progress) {
2012
+ emit({ event: "stuck", reason, iterations_without_progress });
2013
+ }
2014
+ function emitComplete(tasks_done, total_duration_ms) {
2015
+ emit({ event: "complete", tasks_done, total_duration_ms });
2016
+ }
2017
+ function emitFailed(error) {
2018
+ emit({ event: "failed", error });
2019
+ }
2020
+ function emitWarning(type, message, files) {
2021
+ emit({ event: "warning", type, message, files });
2022
+ }
2023
+
2024
+ // src/lib/headless-runner.ts
2025
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
2026
+ import { join as join6 } from "path";
2027
+ var EXIT_CODE_COMPLETE = 0;
2028
+ var EXIT_CODE_STUCK = 1;
2029
+ var EXIT_CODE_MAX_ITERATIONS = 2;
2030
+ var EXIT_CODE_ERROR = 3;
2031
+ function getCompletedTaskTexts(cwd) {
2032
+ const specPath = join6(cwd, "SPEC.md");
2033
+ if (!existsSync5(specPath))
2034
+ return [];
2035
+ const content = readFileSync3(specPath, "utf-8");
2036
+ const lines = content.split(`
2037
+ `);
2038
+ const completedTasks = [];
2039
+ for (const line of lines) {
2040
+ const match = line.match(/^-\s*\[x\]\s+(.+)$/i);
2041
+ if (match) {
2042
+ completedTasks.push(match[1].trim());
2043
+ }
2044
+ }
2045
+ return completedTasks;
2046
+ }
2047
+ function getTotalTaskCount(cwd) {
2048
+ const specPath = join6(cwd, "SPEC.md");
2049
+ if (!existsSync5(specPath))
2050
+ return 0;
2051
+ const content = readFileSync3(specPath, "utf-8");
2052
+ const allTasks = content.match(/^-\s*\[[x\s]\]\s+/gim);
2053
+ return allTasks ? allTasks.length : 0;
2054
+ }
2055
+ var TODO_PATTERNS = [
2056
+ /\/\/\s*TODO:/i,
2057
+ /\/\/\s*FIXME:/i,
2058
+ /#\s*TODO:/i,
2059
+ /#\s*FIXME:/i,
2060
+ /throw new Error\(['"]Not implemented/i,
2061
+ /raise NotImplementedError/i
2062
+ ];
2063
+ function detectTodoStubs(cwd) {
2064
+ const filesWithStubs = [];
2065
+ try {
2066
+ const { execSync: execSync3 } = __require("child_process");
2067
+ const gitDiff = execSync3("git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD", {
2068
+ cwd,
2069
+ encoding: "utf-8"
2070
+ });
2071
+ const changedFiles = gitDiff.split(`
2072
+ `).filter((f) => f.trim()).filter((f) => /\.(ts|tsx|js|jsx|py)$/.test(f));
2073
+ for (const file of changedFiles) {
2074
+ const filePath = join6(cwd, file);
2075
+ if (!existsSync5(filePath))
2076
+ continue;
2077
+ try {
2078
+ const content = readFileSync3(filePath, "utf-8");
2079
+ const hasTodo = TODO_PATTERNS.some((pattern) => pattern.test(content));
2080
+ if (hasTodo) {
2081
+ filesWithStubs.push(file);
2082
+ }
2083
+ } catch {
2084
+ continue;
2085
+ }
2086
+ }
2087
+ } catch {
2088
+ return [];
2089
+ }
2090
+ return filesWithStubs;
2091
+ }
2092
+ async function runSingleIteration(options, iteration) {
2093
+ const startTime = Date.now();
2094
+ const harnessName = options.harness ?? "claude";
2095
+ const harness = getHarness(harnessName);
2096
+ const stats = {
2097
+ toolsStarted: 0,
2098
+ toolsCompleted: 0,
2099
+ toolsErrored: 0,
2100
+ reads: 0,
2101
+ writes: 0,
2102
+ commands: 0,
2103
+ metaOps: 0
2104
+ };
2105
+ let commitHash;
2106
+ let commitMessage;
2107
+ let lastError;
2108
+ const handleEvent = (event) => {
2109
+ switch (event.type) {
2110
+ case "tool_start": {
2111
+ stats.toolsStarted++;
2112
+ const category = getToolCategory(event.name);
2113
+ if (category === "read")
2114
+ stats.reads++;
2115
+ else if (category === "write")
2116
+ stats.writes++;
2117
+ else if (category === "command")
2118
+ stats.commands++;
2119
+ else
2120
+ stats.metaOps++;
2121
+ const toolType = category === "read" ? "read" : category === "write" ? "write" : "bash";
2122
+ emitTool(toolType, event.input);
2123
+ break;
2124
+ }
2125
+ case "tool_end": {
2126
+ stats.toolsCompleted++;
2127
+ if (event.error)
2128
+ stats.toolsErrored++;
2129
+ if (event.name === "Bash" && event.output) {
2130
+ const commitMatch = event.output.match(/\[[\w-]+\s+([a-f0-9]{7,40})\]\s+(.+)/);
2131
+ if (commitMatch) {
2132
+ commitHash = commitMatch[1];
2133
+ commitMessage = commitMatch[2];
2134
+ emitCommit(commitHash, commitMessage);
2135
+ }
2136
+ }
2137
+ break;
2138
+ }
2139
+ case "error": {
2140
+ lastError = new Error(event.message);
2141
+ break;
2142
+ }
2143
+ }
2144
+ };
2145
+ try {
2146
+ const result = await harness.run(options.prompt, {
2147
+ cwd: options.cwd,
2148
+ model: options.model
2149
+ }, handleEvent);
2150
+ if (!result.success && result.error) {
2151
+ lastError = new Error(result.error);
2152
+ }
2153
+ return {
2154
+ iteration,
2155
+ durationMs: result.durationMs || Date.now() - startTime,
2156
+ stats,
2157
+ error: lastError,
2158
+ commitHash,
2159
+ commitMessage
2160
+ };
2161
+ } catch (err) {
2162
+ return {
2163
+ iteration,
2164
+ durationMs: Date.now() - startTime,
2165
+ stats,
2166
+ error: err instanceof Error ? err : new Error(String(err)),
2167
+ commitHash,
2168
+ commitMessage
2169
+ };
2170
+ }
2171
+ }
2172
+ async function executeHeadlessRun(options) {
2173
+ const totalTasks = getTotalTaskCount(options.cwd);
2174
+ const harnessName = options.harness ?? "claude";
2175
+ emitStarted("SPEC.md", totalTasks, options.model, harnessName);
2176
+ let iterationsWithoutProgress = 0;
2177
+ let tasksBefore = getCompletedTaskTexts(options.cwd);
2178
+ const totalStartTime = Date.now();
2179
+ let lastCompletedCount = tasksBefore.length;
2180
+ for (let i = 1;i <= options.iterations; i++) {
2181
+ emitIteration(i, "starting");
2182
+ const result = await runSingleIteration(options, i);
2183
+ if (result.error) {
2184
+ emitFailed(result.error.message);
2185
+ return EXIT_CODE_ERROR;
2186
+ }
2187
+ const tasksAfter = getCompletedTaskTexts(options.cwd);
2188
+ const newlyCompleted = tasksAfter.filter((task) => !tasksBefore.includes(task));
2189
+ for (let j = 0;j < newlyCompleted.length; j++) {
2190
+ emitTaskComplete(lastCompletedCount + j + 1, newlyCompleted[j]);
2191
+ }
2192
+ if (newlyCompleted.length > 0) {
2193
+ const filesWithStubs = detectTodoStubs(options.cwd);
2194
+ if (filesWithStubs.length > 0) {
2195
+ emitWarning("todo_stub", "Completed tasks contain TODO/FIXME stubs", filesWithStubs);
2196
+ }
2197
+ }
2198
+ if (tasksAfter.length > tasksBefore.length) {
2199
+ iterationsWithoutProgress = 0;
2200
+ lastCompletedCount = tasksAfter.length;
2201
+ } else {
2202
+ iterationsWithoutProgress++;
2203
+ }
2204
+ if (result.commitHash && result.commitMessage) {
2205
+ emitCommit(result.commitHash, result.commitMessage);
2206
+ }
2207
+ emitIterationDone(i, result.durationMs, result.stats);
2208
+ if (iterationsWithoutProgress >= options.stuckThreshold) {
2209
+ emitStuck("No task progress", iterationsWithoutProgress);
2210
+ return EXIT_CODE_STUCK;
2211
+ }
2212
+ const specPath = join6(options.cwd, "SPEC.md");
2213
+ if (isSpecComplete(specPath)) {
2214
+ emitComplete(tasksAfter.length, Date.now() - totalStartTime);
2215
+ return EXIT_CODE_COMPLETE;
2216
+ }
2217
+ tasksBefore = tasksAfter;
2218
+ }
2219
+ return EXIT_CODE_MAX_ITERATIONS;
2220
+ }
2221
+
2222
+ // src/lib/spec-validator.ts
2223
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
2224
+ import { join as join7 } from "path";
2225
+ var CODE_FENCE_PATTERN = /^```/;
2226
+ var FILE_LINE_PATTERN = /\b[\w/.-]+\.(ts|js|tsx|jsx|py|go|rs|java|rb|php|c|cpp|h|hpp):\d+/i;
2227
+ var SHELL_COMMAND_PATTERN = /^\s*[-•]\s*(npm|npx|yarn|pnpm|git|docker|kubectl|curl|wget|bash|sh|cd|mkdir|rm|cp|mv|cat|grep|awk|sed)\s+/i;
2228
+ var TECHNICAL_NOTES_PATTERN = /^#{1,4}\s*(Technical\s*Notes?|Implementation\s*Notes?|Fix\s*Approach|How\s*to\s*Fix)/i;
2229
+ var IMPLEMENTATION_KEYWORDS = [
2230
+ /\buse\s+`[^`]+`\s+to\b/i,
2231
+ /\bremove\s+(the|this)\s+(early\s+)?return\b/i,
2232
+ /\badd\s+`[^`]+`\s+(flag|option|parameter)\b/i,
2233
+ /\bchange\s+line\s+\d+\b/i,
2234
+ /\breplace\s+`[^`]+`\s+with\s+`[^`]+`/i,
2235
+ /\b(at|on|in)\s+line\s+\d+\b/i
2236
+ ];
2237
+ function validateSpecContent(content) {
2238
+ const violations = [];
2239
+ const warnings = [];
2240
+ const lines = content.split(`
2241
+ `);
2242
+ let inCodeBlock = false;
2243
+ let codeBlockStart = 0;
2244
+ let codeBlockContent = [];
2245
+ for (let i = 0;i < lines.length; i++) {
2246
+ const line = lines[i];
2247
+ const lineNum = i + 1;
2248
+ if (CODE_FENCE_PATTERN.test(line)) {
2249
+ if (!inCodeBlock) {
2250
+ inCodeBlock = true;
2251
+ codeBlockStart = lineNum;
2252
+ codeBlockContent = [];
2253
+ } else {
2254
+ if (codeBlockContent.length >= 1 && !isExampleBlock(lines, codeBlockStart - 1)) {
2255
+ violations.push({
2256
+ type: "code_snippet",
2257
+ line: codeBlockStart,
2258
+ content: codeBlockContent.slice(0, 3).join(`
2259
+ `) + (codeBlockContent.length > 3 ? `
2260
+ ...` : ""),
2261
+ message: "Code snippets belong in plan.md, not SPEC.md. Describe WHAT to build, not HOW."
2262
+ });
2263
+ }
2264
+ inCodeBlock = false;
2265
+ codeBlockContent = [];
2266
+ }
2267
+ continue;
2268
+ }
2269
+ if (inCodeBlock) {
2270
+ codeBlockContent.push(line);
2271
+ continue;
2272
+ }
2273
+ if (FILE_LINE_PATTERN.test(line)) {
2274
+ violations.push({
2275
+ type: "file_line_reference",
2276
+ line: lineNum,
2277
+ content: line.trim(),
2278
+ message: "File:line references are implementation details. Describe the requirement instead."
2279
+ });
2280
+ }
2281
+ if (SHELL_COMMAND_PATTERN.test(line)) {
2282
+ violations.push({
2283
+ type: "shell_command",
2284
+ line: lineNum,
2285
+ content: line.trim(),
2286
+ message: "Shell commands are implementation details. Describe the outcome instead."
2287
+ });
2288
+ }
2289
+ if (TECHNICAL_NOTES_PATTERN.test(line)) {
2290
+ violations.push({
2291
+ type: "technical_notes_section",
2292
+ line: lineNum,
2293
+ content: line.trim(),
2294
+ message: '"Technical Notes" sections belong in plan.md. SPECs describe requirements only.'
2295
+ });
2296
+ }
2297
+ for (const pattern of IMPLEMENTATION_KEYWORDS) {
2298
+ if (pattern.test(line)) {
2299
+ violations.push({
2300
+ type: "implementation_instruction",
2301
+ line: lineNum,
2302
+ content: line.trim(),
2303
+ message: "This looks like an implementation instruction. Describe the deliverable instead."
2304
+ });
2305
+ break;
2306
+ }
2307
+ }
2308
+ }
2309
+ return {
2310
+ valid: violations.length === 0,
2311
+ violations,
2312
+ warnings
2313
+ };
2314
+ }
2315
+ function isExampleBlock(lines, startIndex) {
2316
+ for (let i = startIndex;i >= Math.max(0, startIndex - 3); i--) {
2317
+ const line = lines[i].toLowerCase();
2318
+ if (/\bexample\b/.test(line) || /^#+\s*(bad|good)\b/.test(line) || /\b(bad|good)\s+example\b/.test(line)) {
2319
+ return true;
2320
+ }
2321
+ }
2322
+ return false;
2323
+ }
2324
+ function validateSpec(specPath) {
2325
+ if (!existsSync6(specPath)) {
2326
+ return {
2327
+ valid: false,
2328
+ violations: [],
2329
+ warnings: [`SPEC.md not found at ${specPath}`]
2330
+ };
2331
+ }
2332
+ const content = readFileSync4(specPath, "utf-8");
2333
+ return validateSpecContent(content);
2334
+ }
2335
+ function validateSpecInDir(dir) {
2336
+ const specPath = join7(dir, "SPEC.md");
2337
+ return validateSpec(specPath);
2338
+ }
2339
+ function formatViolation(v) {
2340
+ return `Line ${v.line}: [${v.type}]
2341
+ ${v.content}
2342
+ → ${v.message}`;
2343
+ }
2344
+ function formatValidationResult(result) {
2345
+ if (result.valid && result.warnings.length === 0) {
2346
+ return "✓ SPEC.md follows conventions";
2347
+ }
2348
+ const parts = [];
2349
+ if (result.warnings.length > 0) {
2350
+ parts.push("Warnings:");
2351
+ parts.push(...result.warnings.map((w) => ` ⚠ ${w}`));
2352
+ }
2353
+ if (result.violations.length > 0) {
2354
+ parts.push(`
2355
+ Found ${result.violations.length} violation(s):
2356
+ `);
2357
+ parts.push(...result.violations.map((v) => formatViolation(v)));
2358
+ }
2359
+ return parts.join(`
2360
+ `);
2361
+ }
2362
+
2363
+ // src/lib/spec-generator.ts
2364
+ import { spawn } from "child_process";
2365
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2366
+ import { join as join8 } from "path";
2367
+ var SPEC_GENERATION_PROMPT = `You are generating a SPEC.md for a Ralphie project. Your task is to create a well-structured specification based on the user's description.
2368
+
2369
+ ## Your Task
2370
+
2371
+ Create a SPEC.md file based on this project description:
2372
+
2373
+ {DESCRIPTION}
2374
+
2375
+ ## Process
2376
+
2377
+ 1. **Analyze the description** - Understand what's being requested
2378
+ 2. **Explore the codebase FIRST** - Before designing tasks, understand what exists:
2379
+ - Read README.md, CLAUDE.md, and any docs/*.md for project context
2380
+ - Run \`ls\` and \`tree\` to see project structure
2381
+ - Use Glob to find relevant files (e.g., \`**/*.ts\`, \`**/routes/*\`)
2382
+ - Read key files to understand existing patterns, conventions, and architecture
2383
+ - Identify what can be reused vs. what needs to be created
2384
+ - Note how similar features are implemented
2385
+ 3. **Design tasks that integrate** - Tasks should fit with existing code:
2386
+ - Follow existing naming conventions
2387
+ - Use existing shared utilities/types
2388
+ - Match existing patterns (e.g., if all routes are in /routes, new ones go there too)
2389
+ 4. **Write SPEC.md** - Create the spec file following the rules below
2390
+ 5. **Validate** - Ensure the spec follows conventions
2391
+
2392
+ ## SPEC Format
2393
+
2394
+ \`\`\`markdown
2395
+ # Project Name
2396
+
2397
+ Brief description (1-2 sentences).
2398
+
2399
+ ## Goal
2400
+ What this project achieves when complete.
2401
+
2402
+ ## Tasks
2403
+
2404
+ ### Phase 1: Foundation
2405
+ - [ ] Task description
2406
+ - Deliverable 1
2407
+ - Deliverable 2
2408
+
2409
+ ### Phase 2: Core Features
2410
+ - [ ] Another task
2411
+ - Deliverable 1
2412
+ \`\`\`
2413
+
2414
+ ## Critical Rules - What NOT to Include
2415
+
2416
+ SPECs describe **requirements**, not solutions. NEVER include:
2417
+
2418
+ - ❌ Code snippets or implementation examples
2419
+ - ❌ File:line references (e.g., \`auth.ts:42\`)
2420
+ - ❌ Shell commands (\`npm install X\`, \`git log\`)
2421
+ - ❌ Root cause analysis ("The bug is because...")
2422
+ - ❌ "Technical Notes" or "Fix Approach" sections
2423
+ - ❌ Implementation instructions ("Use X to...", "Change line Y")
2424
+
2425
+ ## Sub-bullets are Deliverables, NOT Instructions
2426
+
2427
+ \`\`\`markdown
2428
+ # BAD - prescribes HOW
2429
+ - [ ] Fix auth bug
2430
+ - Use \`bcrypt.compare()\` instead of \`===\`
2431
+ - Add try/catch at line 50
2432
+
2433
+ # GOOD - describes WHAT
2434
+ - [ ] Fix auth bug
2435
+ - Password comparison should be timing-safe
2436
+ - Handle comparison errors gracefully
2437
+ \`\`\`
2438
+
2439
+ ## Task Batching
2440
+
2441
+ Each checkbox = one Ralphie iteration. Batch related work:
2442
+
2443
+ \`\`\`markdown
2444
+ # BAD - 4 iterations
2445
+ - [ ] Create UserModel.ts
2446
+ - [ ] Create UserService.ts
2447
+ - [ ] Create UserController.ts
2448
+ - [ ] Create user.test.ts
2449
+
2450
+ # GOOD - 1 iteration
2451
+ - [ ] Create User module (Model, Service, Controller) with tests
2452
+ \`\`\`
2453
+
2454
+ ## Verification Steps
2455
+
2456
+ Each task SHOULD include a **Verify:** section with concrete checks:
2457
+
2458
+ \`\`\`markdown
2459
+ - [ ] Implement authentication system
2460
+ - POST /auth/register - create user with hashed password
2461
+ - POST /auth/login - validate credentials, return JWT
2462
+ - Tests for all auth flows
2463
+
2464
+ **Verify:**
2465
+ - \`curl -X POST localhost:3000/auth/register -d '{"email":"test@test.com","password":"test123"}'\` → 201
2466
+ - \`curl -X POST localhost:3000/auth/login -d '{"email":"test@test.com","password":"test123"}'\` → returns JWT
2467
+ - \`npm test\` → all tests pass
2468
+ \`\`\`
2469
+
2470
+ Good verification steps:
2471
+ - API calls with expected response codes
2472
+ - CLI commands with expected output
2473
+ - File existence checks (\`ls dist/\` → contains index.js)
2474
+ - Test commands (\`npm test\` → all pass)
2475
+
2476
+ ## Output
2477
+
2478
+ After writing SPEC.md, output a summary:
2479
+ - Number of phases
2480
+ - Number of tasks
2481
+ - Estimated complexity`;
2482
+ var HEADLESS_ADDENDUM = `
2483
+
2484
+ Do NOT ask questions. Make reasonable assumptions based on the description. If something is ambiguous, choose the simpler option.
2485
+
2486
+ Write the SPEC.md now.`;
2487
+ function emitJson(event) {
2488
+ console.log(JSON.stringify({ ...event, timestamp: new Date().toISOString() }));
2489
+ }
2490
+ async function generateSpec(options) {
2491
+ if (options.autonomous) {
2492
+ return generateSpecAutonomous(options);
2493
+ }
2494
+ const useSkill = !options.headless;
2495
+ const prompt = useSkill ? `/create-spec
2496
+
2497
+ Description: ${options.description}` : SPEC_GENERATION_PROMPT.replace("{DESCRIPTION}", options.description) + HEADLESS_ADDENDUM;
2498
+ if (options.headless) {
2499
+ emitJson({ event: "spec_generation_started", description: options.description });
2500
+ } else {
2501
+ console.log(`Generating SPEC for: ${options.description}
2502
+ `);
2503
+ }
2504
+ if (!options.headless) {
2505
+ return generateSpecInteractive(options, prompt);
2506
+ }
2507
+ return generateSpecHeadless(options, prompt);
2508
+ }
2509
+ async function generateSpecInteractive(options, prompt) {
2510
+ return new Promise((resolve) => {
2511
+ const args = [
2512
+ "--dangerously-skip-permissions",
2513
+ ...options.model ? ["--model", options.model] : []
2514
+ ];
2515
+ const proc = spawn("claude", args, {
2516
+ cwd: options.cwd,
2517
+ env: process.env,
2518
+ stdio: ["pipe", "inherit", "inherit"]
2519
+ });
2520
+ proc.stdin?.write(prompt + `
2521
+ `);
2522
+ process.stdin.setRawMode?.(false);
2523
+ process.stdin.resume();
2524
+ process.stdin.pipe(proc.stdin);
2525
+ const timeout = setTimeout(() => {
2526
+ proc.kill("SIGTERM");
2527
+ resolve({
2528
+ success: false,
2529
+ error: `Timeout: no progress for ${options.timeoutMs / 1000}s`
2530
+ });
2531
+ }, options.timeoutMs);
2532
+ proc.on("close", (code) => {
2533
+ clearTimeout(timeout);
2534
+ process.stdin.unpipe(proc.stdin);
2535
+ process.stdin.pause();
2536
+ const specPath = join8(options.cwd, "SPEC.md");
2537
+ if (existsSync7(specPath)) {
2538
+ const content = readFileSync5(specPath, "utf-8");
2539
+ const taskCount = (content.match(/^- \[ \]/gm) || []).length;
2540
+ const validation = validateSpecInDir(options.cwd);
2541
+ const validationOutput = formatValidationResult(validation);
2542
+ console.log(`
2543
+ Validation:`);
2544
+ console.log(validationOutput);
2545
+ resolve({
2546
+ success: true,
2547
+ specPath,
2548
+ taskCount,
2549
+ validationPassed: validation.valid,
2550
+ validationOutput
2551
+ });
2552
+ } else {
2553
+ resolve({
2554
+ success: false,
2555
+ error: code === 0 ? "SPEC.md was not created" : `Claude exited with code ${code}`
2556
+ });
2557
+ }
2558
+ });
2559
+ });
2560
+ }
2561
+ async function generateSpecHeadless(options, prompt) {
2562
+ return new Promise((resolve) => {
2563
+ const args = [
2564
+ "--dangerously-skip-permissions",
2565
+ "--output-format",
2566
+ "stream-json",
2567
+ "--verbose",
2568
+ ...options.model ? ["--model", options.model] : [],
2569
+ "-p",
2570
+ prompt
2571
+ ];
2572
+ const proc = spawn("claude", args, {
2573
+ cwd: options.cwd,
2574
+ env: process.env,
2575
+ stdio: ["pipe", "pipe", "pipe"]
2576
+ });
2577
+ proc.stdin?.end();
2578
+ let output = "";
2579
+ let lastOutput = Date.now();
2580
+ const timeout = setTimeout(() => {
2581
+ proc.kill("SIGTERM");
2582
+ resolve({
2583
+ success: false,
2584
+ error: `Timeout: no progress for ${options.timeoutMs / 1000}s`
2585
+ });
2586
+ }, options.timeoutMs);
2587
+ proc.stdout?.on("data", (data) => {
2588
+ lastOutput = Date.now();
2589
+ output += data.toString();
2590
+ if (!options.headless) {
2591
+ const lines = data.toString().split(`
2592
+ `);
2593
+ for (const line of lines) {
2594
+ if (line.trim()) {
2595
+ try {
2596
+ const parsed = JSON.parse(line);
2597
+ if (parsed.type === "assistant" && parsed.message?.content) {
2598
+ for (const block of parsed.message.content) {
2599
+ if (block.type === "text") {
2600
+ process.stdout.write(".");
2601
+ } else if (block.type === "tool_use") {
2602
+ process.stdout.write(`
2603
+ [${block.name}] `);
2604
+ }
2605
+ }
2606
+ }
2607
+ } catch {}
2608
+ }
2609
+ }
2610
+ }
2611
+ });
2612
+ proc.stderr?.on("data", (data) => {
2613
+ lastOutput = Date.now();
2614
+ if (!options.headless) {
2615
+ process.stderr.write(data);
2616
+ }
2617
+ });
2618
+ proc.on("close", (code) => {
2619
+ clearTimeout(timeout);
2620
+ if (!options.headless) {
2621
+ console.log(`
2622
+ `);
2623
+ }
2624
+ const specPath = join8(options.cwd, "SPEC.md");
2625
+ if (!existsSync7(specPath)) {
2626
+ if (options.headless) {
2627
+ emitJson({ event: "spec_generation_failed", error: "SPEC.md was not created" });
2628
+ }
2629
+ resolve({
2630
+ success: false,
2631
+ error: "SPEC.md was not created"
2632
+ });
2633
+ return;
2634
+ }
2635
+ const specContent = readFileSync5(specPath, "utf-8");
2636
+ const taskMatches = specContent.match(/^-\s*\[\s*\]\s+/gm);
2637
+ const taskCount = taskMatches ? taskMatches.length : 0;
2638
+ const validation = validateSpecInDir(options.cwd);
2639
+ const validationOutput = formatValidationResult(validation);
2640
+ if (options.headless) {
2641
+ emitJson({
2642
+ event: "spec_generation_complete",
2643
+ specPath,
2644
+ taskCount,
2645
+ validationPassed: validation.valid,
2646
+ violations: validation.violations.length
2647
+ });
2648
+ } else {
2649
+ console.log(`SPEC.md created with ${taskCount} tasks
2650
+ `);
2651
+ console.log("Validation:");
2652
+ console.log(validationOutput);
2653
+ }
2654
+ resolve({
2655
+ success: true,
2656
+ specPath,
2657
+ taskCount,
2658
+ validationPassed: validation.valid,
2659
+ validationOutput
2660
+ });
2661
+ });
2662
+ proc.on("error", (err) => {
2663
+ clearTimeout(timeout);
2664
+ if (options.headless) {
2665
+ emitJson({ event: "spec_generation_failed", error: err.message });
2666
+ }
2667
+ resolve({
2668
+ success: false,
2669
+ error: err.message
2670
+ });
2671
+ });
2672
+ });
2673
+ }
2674
+ async function runReviewSpec(specPath, cwd, model) {
2675
+ return new Promise((resolve) => {
2676
+ const args = [
2677
+ "--dangerously-skip-permissions",
2678
+ "--output-format",
2679
+ "stream-json",
2680
+ "--verbose",
2681
+ ...model ? ["--model", model] : [],
2682
+ "-p",
2683
+ `/review-spec ${specPath}`
2684
+ ];
2685
+ const proc = spawn("claude", args, {
2686
+ cwd,
2687
+ env: process.env,
2688
+ stdio: ["pipe", "pipe", "pipe"]
2689
+ });
2690
+ proc.stdin?.end();
2691
+ let output = "";
2692
+ proc.stdout?.on("data", (data) => {
2693
+ output += data.toString();
2694
+ });
2695
+ proc.on("close", () => {
2696
+ const result = parseReviewOutput(output);
2697
+ resolve(result);
2698
+ });
2699
+ proc.on("error", () => {
2700
+ resolve({
2701
+ passed: false,
2702
+ concerns: ["Failed to run review-spec skill"],
2703
+ fullOutput: output
2704
+ });
2705
+ });
2706
+ });
2707
+ }
2708
+ function parseReviewOutput(output) {
2709
+ const passMatch = /SPEC Review:\s*PASS/i.test(output);
2710
+ const failMatch = /SPEC Review:\s*FAIL/i.test(output);
2711
+ if (passMatch && !failMatch) {
2712
+ return {
2713
+ passed: true,
2714
+ concerns: [],
2715
+ fullOutput: output
2716
+ };
2717
+ }
2718
+ const concerns = [];
2719
+ const formatSection = output.match(/## Format Issues\s+([\s\S]*?)(?=##|$)/i);
2720
+ if (formatSection) {
2721
+ concerns.push(`Format issues found:
2722
+ ` + formatSection[1].trim());
2723
+ }
2724
+ const contentSection = output.match(/## Content Concerns\s+([\s\S]*?)(?=##|$)/i);
2725
+ if (contentSection) {
2726
+ concerns.push(`Content concerns:
2727
+ ` + contentSection[1].trim());
2728
+ }
2729
+ const recommendationsSection = output.match(/## Recommendations\s+([\s\S]*?)(?=##|$)/i);
2730
+ if (recommendationsSection) {
2731
+ concerns.push(`Recommendations:
2732
+ ` + recommendationsSection[1].trim());
2733
+ }
2734
+ return {
2735
+ passed: false,
2736
+ concerns: concerns.length > 0 ? concerns : ["Review failed but no specific concerns extracted"],
2737
+ fullOutput: output
2738
+ };
2739
+ }
2740
+ async function refineSpec(description, currentSpec, concerns, cwd, model) {
2741
+ const refinementPrompt = `You previously generated a SPEC.md that has issues. Please revise it based on this feedback:
2742
+
2743
+ ${concerns.join(`
2744
+
2745
+ `)}
2746
+
2747
+ Original Description: ${description}
2748
+
2749
+ Current SPEC content:
2750
+ ${currentSpec}
2751
+
2752
+ Generate an improved SPEC.md that addresses all the concerns above. Write the updated SPEC.md to the file.`;
2753
+ return new Promise((resolve) => {
2754
+ const args = [
2755
+ "--dangerously-skip-permissions",
2756
+ "--output-format",
2757
+ "stream-json",
2758
+ "--verbose",
2759
+ ...model ? ["--model", model] : [],
2760
+ "-p",
2761
+ refinementPrompt
2762
+ ];
2763
+ const proc = spawn("claude", args, {
2764
+ cwd,
2765
+ env: process.env,
2766
+ stdio: ["pipe", "pipe", "pipe"]
2767
+ });
2768
+ proc.stdin?.end();
2769
+ proc.on("close", (code) => {
2770
+ const specPath = join8(cwd, "SPEC.md");
2771
+ resolve(code === 0 && existsSync7(specPath));
2772
+ });
2773
+ proc.on("error", () => {
2774
+ resolve(false);
2775
+ });
2776
+ });
2777
+ }
2778
+ async function generateSpecAutonomous(options) {
2779
+ const maxAttempts = options.maxAttempts ?? 3;
2780
+ const specPath = join8(options.cwd, "SPEC.md");
2781
+ if (options.headless) {
2782
+ emitJson({ event: "autonomous_spec_started", description: options.description, maxAttempts });
2783
+ } else {
2784
+ console.log(`Generating SPEC autonomously: ${options.description}`);
2785
+ console.log(`Max attempts: ${maxAttempts}
2786
+ `);
2787
+ }
2788
+ if (!options.headless) {
2789
+ console.log("Attempt 1: Generating initial SPEC...");
2790
+ }
2791
+ const initialPrompt = SPEC_GENERATION_PROMPT.replace("{DESCRIPTION}", options.description) + HEADLESS_ADDENDUM;
2792
+ const initialResult = await generateSpecHeadless({ ...options, headless: true }, initialPrompt);
2793
+ if (!initialResult.success) {
2794
+ if (options.headless) {
2795
+ emitJson({ event: "autonomous_spec_failed", error: initialResult.error, attempts: 1 });
2796
+ }
2797
+ return {
2798
+ ...initialResult,
2799
+ attempts: 1
2800
+ };
2801
+ }
2802
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2803
+ if (!options.headless) {
2804
+ console.log(`
2805
+ Attempt ${attempt}: Running review...`);
2806
+ }
2807
+ if (options.headless) {
2808
+ emitJson({ event: "autonomous_review_started", attempt });
2809
+ }
2810
+ const reviewResult = await runReviewSpec(specPath, options.cwd, options.model);
2811
+ if (reviewResult.passed) {
2812
+ if (!options.headless) {
2813
+ console.log(`
2814
+ ✓ Review passed on attempt ${attempt}!`);
2815
+ }
2816
+ if (options.headless) {
2817
+ emitJson({ event: "autonomous_spec_complete", attempts: attempt, reviewPassed: true });
2818
+ }
2819
+ return {
2820
+ success: true,
2821
+ specPath,
2822
+ taskCount: initialResult.taskCount,
2823
+ validationPassed: initialResult.validationPassed,
2824
+ reviewPassed: true,
2825
+ attempts: attempt
2826
+ };
2827
+ }
2828
+ if (!options.headless) {
2829
+ console.log(`✗ Review failed on attempt ${attempt}`);
2830
+ console.log("Concerns:");
2831
+ for (const concern of reviewResult.concerns) {
2832
+ console.log(` - ${concern.split(`
2833
+ `)[0]}`);
2834
+ }
2835
+ }
2836
+ if (options.headless) {
2837
+ emitJson({
2838
+ event: "autonomous_review_failed",
2839
+ attempt,
2840
+ concerns: reviewResult.concerns.length
2841
+ });
2842
+ }
2843
+ if (attempt === maxAttempts) {
2844
+ if (options.headless) {
2845
+ emitJson({
2846
+ event: "autonomous_spec_failed",
2847
+ error: "Max attempts reached without passing review",
2848
+ attempts: maxAttempts
2849
+ });
2850
+ } else {
2851
+ console.log(`
2852
+ ✗ Failed to generate valid SPEC after ${maxAttempts} attempts`);
2853
+ }
2854
+ return {
2855
+ success: false,
2856
+ error: `Max attempts (${maxAttempts}) reached without passing review`,
2857
+ reviewPassed: false,
2858
+ attempts: maxAttempts
2859
+ };
2860
+ }
2861
+ if (!options.headless) {
2862
+ console.log(`
2863
+ Attempt ${attempt + 1}: Refining SPEC...`);
2864
+ }
2865
+ const currentSpec = readFileSync5(specPath, "utf-8");
2866
+ const refined = await refineSpec(options.description, currentSpec, reviewResult.concerns, options.cwd, options.model);
2867
+ if (!refined) {
2868
+ if (options.headless) {
2869
+ emitJson({
2870
+ event: "autonomous_spec_failed",
2871
+ error: "Refinement failed",
2872
+ attempts: attempt + 1
2873
+ });
2874
+ }
2875
+ return {
2876
+ success: false,
2877
+ error: "Failed to refine SPEC",
2878
+ attempts: attempt + 1
2879
+ };
2880
+ }
2881
+ }
2882
+ return {
2883
+ success: false,
2884
+ error: "Unexpected error in autonomous generation",
2885
+ attempts: maxAttempts
2886
+ };
2887
+ }
2888
+
2889
+ // src/lib/config-loader.ts
2890
+ import fs from "fs";
2891
+ import path from "path";
2892
+ import yaml from "js-yaml";
2893
+ var VALID_HARNESSES = ["claude", "codex"];
2894
+ function getHarnessName(cliHarness, cwd) {
2895
+ const candidates = [
2896
+ cliHarness,
2897
+ process.env.RALPH_HARNESS,
2898
+ loadConfig(cwd)?.harness
2899
+ ];
2900
+ for (const candidate of candidates) {
2901
+ if (candidate && VALID_HARNESSES.includes(candidate)) {
2902
+ return candidate;
2903
+ }
2904
+ }
2905
+ return "claude";
2906
+ }
2907
+ function loadConfig(cwd) {
2908
+ const configPath = path.join(cwd, ".ralphie", "config.yml");
2909
+ try {
2910
+ if (!fs.existsSync(configPath)) {
2911
+ return null;
2912
+ }
2913
+ const content = fs.readFileSync(configPath, "utf-8");
2914
+ const config = yaml.load(content);
2915
+ return config;
2916
+ } catch (error) {
2917
+ return null;
2918
+ }
2919
+ }
2920
+
2921
+ // src/cli.tsx
2922
+ import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime";
2923
+ var __filename4 = fileURLToPath3(import.meta.url);
2924
+ var __dirname4 = dirname3(__filename4);
2925
+ function getVersion() {
2926
+ try {
2927
+ let pkgPath = join9(__dirname4, "..", "package.json");
2928
+ if (!existsSync8(pkgPath)) {
2929
+ pkgPath = join9(__dirname4, "..", "..", "package.json");
2930
+ }
2931
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
2932
+ return pkg.version || "1.0.0";
2933
+ } catch {
2934
+ return "1.0.0";
2935
+ }
2936
+ }
2937
+ var DEFAULT_PROMPT = `You are Ralphie, an autonomous coding assistant.
2938
+
2939
+ ## Your Task
2940
+ Complete ONE checkbox from SPEC.md per iteration. Sub-bullets under a checkbox are implementation details - complete ALL of them before marking the checkbox done.
2941
+
2942
+ ## The Loop
2943
+ 1. Read SPEC.md to find the next incomplete task (check STATE.txt if unsure)
2944
+ 2. Write plan to .ai/ralphie/plan.md:
2945
+ - Goal: one sentence
2946
+ - Files: what you'll create/modify
2947
+ - Tests: what you'll test
2948
+ - Exit criteria: how you know you're done
2949
+ 3. Implement the task with tests
2950
+ 4. Run tests and type checks
2951
+ 5. Mark checkbox complete in SPEC.md
2952
+ 6. Commit with clear message
2953
+ 7. Update .ai/ralphie/index.md (append commit summary) and STATE.txt
2954
+
2955
+ ## Memory Files
2956
+ - .ai/ralphie/plan.md - Current task plan (overwrite each iteration)
2957
+ - .ai/ralphie/index.md - Commit log (append after each commit)
2958
+
2959
+ ## Rules
2960
+ - Plan BEFORE coding
2961
+ - Tests BEFORE marking complete
2962
+ - Commit AFTER each task
2963
+ - No TODO/FIXME stubs in completed tasks`;
2964
+ var GREEDY_PROMPT = `You are Ralphie, an autonomous coding assistant in GREEDY MODE.
2965
+
2966
+ ## Your Task
2967
+ Complete AS MANY checkboxes as possible from SPEC.md before context fills up.
2968
+
2969
+ ## The Loop (repeat until done or context full)
2970
+ 1. Read SPEC.md to find the next incomplete task
2971
+ 2. Write plan to .ai/ralphie/plan.md
2972
+ 3. Implement the task with tests
2973
+ 4. Run tests and type checks
2974
+ 5. Mark checkbox complete in SPEC.md
2975
+ 6. Commit with clear message
2976
+ 7. Update .ai/ralphie/index.md and STATE.txt
2977
+ 8. **CONTINUE to next task** (don't stop!)
2978
+
2979
+ ## Memory Files
2980
+ - .ai/ralphie/plan.md - Current task plan (overwrite each task)
2981
+ - .ai/ralphie/index.md - Commit log (append after each commit)
2982
+
2983
+ ## Rules
2984
+ - Commit after EACH task (saves progress incrementally)
2985
+ - Keep going until all tasks done OR context is filling up
2986
+ - No TODO/FIXME stubs in completed tasks
2987
+ - The goal is maximum throughput - don't stop after one task`;
2988
+ var MAX_ALL_ITERATIONS = 100;
2989
+ function resolvePrompt(options) {
2990
+ if (options.prompt) {
2991
+ return options.prompt;
2992
+ }
2993
+ if (options.promptFile) {
2994
+ const filePath = resolve(options.cwd, options.promptFile);
2995
+ if (!existsSync8(filePath)) {
2996
+ throw new Error(`Prompt file not found: ${filePath}`);
2997
+ }
2998
+ return readFileSync6(filePath, "utf-8");
2999
+ }
3000
+ return options.greedy ? GREEDY_PROMPT : DEFAULT_PROMPT;
3001
+ }
3002
+ function executeRun(options) {
3003
+ const validation = validateProject(options.cwd);
3004
+ if (!validation.valid) {
3005
+ console.error("Cannot run Ralphie:");
3006
+ for (const error of validation.errors) {
3007
+ console.error(` - ${error}`);
3008
+ }
3009
+ process.exit(1);
3010
+ }
3011
+ if (!options.noBranch) {
3012
+ const specPath = join9(options.cwd, "SPEC.md");
3013
+ const title = getSpecTitle(specPath);
3014
+ if (title) {
3015
+ const result = createFeatureBranch(options.cwd, title);
3016
+ if (result.created) {
3017
+ console.log(`Created branch: ${result.branchName}`);
3018
+ } else if (result.branchName) {
3019
+ console.log(`Using branch: ${result.branchName}`);
3020
+ } else if (result.error) {
3021
+ console.warn(`Warning: ${result.error}`);
3022
+ }
3023
+ }
3024
+ }
3025
+ const prompt = resolvePrompt(options);
3026
+ const idleTimeoutMs = options.timeoutIdle * 1000;
3027
+ const harness = options.harness ? getHarnessName(options.harness, options.cwd) : undefined;
3028
+ const { waitUntilExit, unmount } = render(/* @__PURE__ */ jsxDEV12(IterationRunner, {
3029
+ prompt,
3030
+ totalIterations: options.iterations,
3031
+ cwd: options.cwd,
3032
+ idleTimeoutMs,
3033
+ saveJsonl: options.saveJsonl,
3034
+ model: options.model,
3035
+ harness
3036
+ }, undefined, false, undefined, this));
3037
+ const handleSignal = () => {
3038
+ unmount();
3039
+ process.exit(0);
3040
+ };
3041
+ process.on("SIGINT", handleSignal);
3042
+ process.on("SIGTERM", handleSignal);
3043
+ waitUntilExit().then(() => {
3044
+ process.exit(0);
3045
+ });
3046
+ }
3047
+ async function executeHeadlessRun2(options) {
3048
+ const validation = validateProject(options.cwd);
3049
+ if (!validation.valid) {
3050
+ emitFailed(`Invalid project: ${validation.errors.join(", ")}`);
3051
+ process.exit(3);
3052
+ }
3053
+ const prompt = resolvePrompt(options);
3054
+ const harness = options.harness ? getHarnessName(options.harness, options.cwd) : undefined;
3055
+ const exitCode = await executeHeadlessRun({
3056
+ prompt,
3057
+ cwd: options.cwd,
3058
+ iterations: options.iterations,
3059
+ stuckThreshold: options.stuckThreshold,
3060
+ idleTimeoutMs: options.timeoutIdle * 1000,
3061
+ saveJsonl: options.saveJsonl,
3062
+ model: options.model,
3063
+ harness
3064
+ });
3065
+ process.exit(exitCode);
3066
+ }
3067
+ function main() {
3068
+ const program = new Command;
3069
+ program.name("ralphie").description("Autonomous AI coding loops").version(getVersion());
3070
+ program.command("init").description("Initialize Ralphie in the current directory").argument("[directory]", "Target directory", process.cwd()).action((directory) => {
3071
+ const targetDir = resolve(directory);
3072
+ console.log(`Initializing Ralphie in ${targetDir}...
3073
+ `);
3074
+ try {
3075
+ const result = runInit(targetDir);
3076
+ if (result.created.length > 0) {
3077
+ console.log("Created:");
3078
+ for (const file of result.created) {
3079
+ console.log(` + ${file}`);
3080
+ }
3081
+ }
3082
+ if (result.skipped.length > 0) {
3083
+ console.log(`
3084
+ Skipped (already exist):`);
3085
+ for (const file of result.skipped) {
3086
+ console.log(` - ${file}`);
3087
+ }
3088
+ }
3089
+ console.log(`
3090
+ Ralphie initialized! Next steps:`);
3091
+ console.log(" 1. Create SPEC.md with your project tasks");
3092
+ console.log(" 2. Run: ralphie run");
3093
+ } catch (error) {
3094
+ console.error("Error:", error instanceof Error ? error.message : error);
3095
+ process.exit(1);
3096
+ }
3097
+ });
3098
+ program.command("run").description("Run Ralphie iterations").option("-n, --iterations <number>", "Number of iterations to run", "1").option("-a, --all", "Run until all PRD tasks are complete (max 100 iterations)").option("-p, --prompt <text>", "Prompt to send to Claude").option("--prompt-file <path>", "Read prompt from file").option("--cwd <path>", "Working directory for Claude", process.cwd()).option("--timeout-idle <seconds>", "Kill process after N seconds of no output", "120").option("--save-jsonl <path>", "Save raw JSONL output to file").option("--quiet", "Suppress output (just run iterations)", false).option("--title <text>", "Override task title display").option("--no-branch", "Skip feature branch creation").option("--headless", "Output JSON events instead of UI").option("--stuck-threshold <n>", "Iterations without progress before stuck (headless)", "3").option("-m, --model <name>", "Claude model to use (sonnet, opus, haiku)", "sonnet").option("--harness <name>", "AI harness to use (claude, codex)").option("-g, --greedy", "Complete multiple tasks per iteration until context fills").action((opts) => {
3099
+ let iterations = parseInt(opts.iterations, 10);
3100
+ const all = opts.all ?? false;
3101
+ if (all) {
3102
+ iterations = MAX_ALL_ITERATIONS;
3103
+ console.log(`Running until PRD complete (max ${MAX_ALL_ITERATIONS} iterations)...
3104
+ `);
3105
+ }
3106
+ const options = {
3107
+ iterations,
3108
+ all,
3109
+ prompt: opts.prompt,
3110
+ promptFile: opts.promptFile,
3111
+ cwd: resolve(opts.cwd),
3112
+ timeoutIdle: parseInt(opts.timeoutIdle, 10),
3113
+ saveJsonl: opts.saveJsonl,
3114
+ quiet: opts.quiet,
3115
+ title: opts.title,
3116
+ noBranch: opts.branch === false,
3117
+ headless: opts.headless ?? false,
3118
+ stuckThreshold: parseInt(opts.stuckThreshold, 10),
3119
+ model: opts.model,
3120
+ harness: opts.harness,
3121
+ greedy: opts.greedy ?? false
3122
+ };
3123
+ if (options.headless) {
3124
+ executeHeadlessRun2(options);
3125
+ } else {
3126
+ executeRun(options);
3127
+ }
3128
+ });
3129
+ program.command("validate").description("Check if current directory is ready for Ralphie and validate SPEC.md conventions").option("--cwd <path>", "Working directory to check", process.cwd()).option("--spec-only", "Only validate SPEC.md content (skip project structure check)", false).action((opts) => {
3130
+ const cwd = resolve(opts.cwd);
3131
+ let hasErrors = false;
3132
+ if (!opts.specOnly) {
3133
+ const projectResult = validateProject(cwd);
3134
+ if (!projectResult.valid) {
3135
+ console.log("Project structure issues:");
3136
+ for (const error of projectResult.errors) {
3137
+ console.log(` - ${error}`);
3138
+ }
3139
+ hasErrors = true;
3140
+ } else {
3141
+ console.log("✓ Project structure is valid");
3142
+ }
3143
+ }
3144
+ const specResult = validateSpecInDir(cwd);
3145
+ console.log(`
3146
+ SPEC.md content validation:`);
3147
+ console.log(formatValidationResult(specResult));
3148
+ if (!specResult.valid) {
3149
+ hasErrors = true;
3150
+ }
3151
+ if (hasErrors) {
3152
+ process.exit(1);
3153
+ }
3154
+ });
3155
+ program.command("spec").description("Generate a SPEC.md autonomously from a description").argument("<description>", 'What to build (e.g., "REST API for user management")').option("--cwd <path>", "Working directory", process.cwd()).option("--headless", "Output JSON events instead of UI", false).option("--auto", "Autonomous mode with review loop (no user interaction)", false).option("--timeout <seconds>", "Timeout for generation", "300").option("--max-attempts <n>", "Max refinement attempts in autonomous mode", "3").option("-m, --model <name>", "Claude model to use (sonnet, opus, haiku)", "opus").action(async (description, opts) => {
3156
+ const cwd = resolve(opts.cwd);
3157
+ const result = await generateSpec({
3158
+ description,
3159
+ cwd,
3160
+ headless: opts.headless ?? false,
3161
+ autonomous: opts.auto ?? false,
3162
+ timeoutMs: parseInt(opts.timeout, 10) * 1000,
3163
+ maxAttempts: parseInt(opts.maxAttempts, 10),
3164
+ model: opts.model
3165
+ });
3166
+ if (!result.success) {
3167
+ if (!opts.headless) {
3168
+ console.error(`Failed: ${result.error}`);
3169
+ }
3170
+ process.exit(1);
3171
+ }
3172
+ if (!result.validationPassed && !opts.headless) {
3173
+ console.log("\nWarning: SPEC has convention violations. Run `ralphie validate` for details.");
3174
+ }
3175
+ process.exit(0);
3176
+ });
3177
+ program.command("upgrade").description(`Upgrade a Ralphie project to the latest version (v${CURRENT_VERSION})`).argument("[directory]", "Target directory", process.cwd()).option("--dry-run", "Show what would be changed without making changes", false).option("--clean", "Remove legacy files after confirming project is at latest version", false).action((directory, opts) => {
3178
+ const targetDir = resolve(directory);
3179
+ const detection = detectVersion(targetDir);
3180
+ if (detection.detectedVersion === null) {
3181
+ console.log("Could not detect Ralphie project version.");
3182
+ console.log("If this is a new project, use: ralphie init");
3183
+ return;
3184
+ }
3185
+ if (detection.isLatest && !detection.hasLegacyFiles) {
3186
+ const claudeDir = resolve(targetDir, ".claude");
3187
+ const ralphieMdPath = resolve(claudeDir, "ralphie.md");
3188
+ if (existsSync8(ralphieMdPath)) {
3189
+ const content = readFileSync6(ralphieMdPath, "utf-8");
3190
+ const hasOldPatterns = /\bPRD\b/.test(content) || /\bprogress\.txt\b/.test(content);
3191
+ if (hasOldPatterns) {
3192
+ console.log(`Project is at ${getVersionName(detection.detectedVersion)} but .claude/ralphie.md has old patterns.`);
3193
+ if (opts.clean) {
3194
+ const templatesDir = resolve(__dirname4, "..", "templates");
3195
+ const templatePath = resolve(templatesDir, ".claude", "ralphie.md");
3196
+ if (existsSync8(templatePath)) {
3197
+ copyFileSync3(templatePath, ralphieMdPath);
3198
+ console.log("Updated .claude/ralphie.md to v2 template.");
3199
+ }
3200
+ } else {
3201
+ console.log("Run with --clean to update it.");
3202
+ }
3203
+ return;
3204
+ }
3205
+ }
3206
+ console.log(`Project is already at ${getVersionName(detection.detectedVersion)} (latest)`);
3207
+ return;
3208
+ }
3209
+ if (detection.isLatest && detection.hasLegacyFiles) {
3210
+ console.log(`Project is at ${getVersionName(detection.detectedVersion)} but has legacy files:`);
3211
+ for (const file of detection.legacyFiles) {
3212
+ console.log(` - ${file}`);
3213
+ }
3214
+ console.log(`
3215
+ Run with --clean to remove them, or delete manually.`);
3216
+ if (opts.clean) {
3217
+ console.log(`
3218
+ Cleaning up legacy files...`);
3219
+ for (const file of detection.legacyFiles) {
3220
+ const filePath = resolve(targetDir, file);
3221
+ try {
3222
+ unlinkSync(filePath);
3223
+ console.log(` Removed: ${file}`);
3224
+ } catch {
3225
+ console.log(` Failed to remove: ${file}`);
3226
+ }
3227
+ }
3228
+ console.log(`
3229
+ Cleanup complete!`);
3230
+ }
3231
+ return;
3232
+ }
3233
+ console.log(`Detected: ${getVersionName(detection.detectedVersion)}`);
3234
+ console.log(`Target: ${getVersionName(CURRENT_VERSION)}
3235
+ `);
3236
+ console.log(`Found files: ${detection.foundIndicators.join(", ")}
3237
+ `);
3238
+ if (opts.dryRun) {
3239
+ console.log("Dry run - would upgrade from:");
3240
+ console.log(` ${getVersionName(detection.detectedVersion)} → ${getVersionName(CURRENT_VERSION)}`);
3241
+ return;
3242
+ }
3243
+ try {
3244
+ const result = runUpgrade(targetDir);
3245
+ console.log(`Upgraded: v${result.fromVersion} → v${result.toVersion}
3246
+ `);
3247
+ if (result.renamed.length > 0) {
3248
+ console.log("Renamed:");
3249
+ for (const { from, to } of result.renamed) {
3250
+ console.log(` ${from} → ${to}`);
3251
+ }
3252
+ }
3253
+ if (result.created.length > 0) {
3254
+ console.log(`
3255
+ Created:`);
3256
+ for (const file of result.created) {
3257
+ console.log(` + ${file}`);
3258
+ }
3259
+ }
3260
+ if (result.skipped.length > 0) {
3261
+ console.log(`
3262
+ Skipped:`);
3263
+ for (const file of result.skipped) {
3264
+ console.log(` - ${file}`);
3265
+ }
3266
+ }
3267
+ if (result.warnings.length > 0) {
3268
+ console.log(`
3269
+ Warnings:`);
3270
+ for (const warning of result.warnings) {
3271
+ console.log(` ⚠ ${warning}`);
3272
+ }
3273
+ }
3274
+ console.log(`
3275
+ Upgrade complete! Run: ralphie validate`);
3276
+ } catch (error) {
3277
+ console.error("Error:", error instanceof Error ? error.message : error);
3278
+ process.exit(1);
3279
+ }
3280
+ });
3281
+ if (process.argv.length === 2) {
3282
+ program.help();
3283
+ }
3284
+ program.parse(process.argv);
3285
+ }
3286
+ if (true) {
3287
+ main();
3288
+ }
3289
+ export {
3290
+ resolvePrompt,
3291
+ executeRun,
3292
+ executeHeadlessRun2 as executeHeadlessRun,
3293
+ MAX_ALL_ITERATIONS,
3294
+ GREEDY_PROMPT,
3295
+ DEFAULT_PROMPT
3296
+ };