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/LICENSE +21 -0
- package/README.md +110 -0
- package/bin/ralphie +2 -0
- package/dist/cli.js +3296 -0
- package/package.json +67 -0
- package/skills/create-spec/SKILL.md +222 -0
- package/skills/ralphie-iterate/SKILL.md +959 -0
- package/skills/review-spec/SKILL.md +390 -0
- package/skills/verify/SKILL.md +496 -0
- package/templates/.ai/ralphie/.gitkeep +0 -0
- package/templates/.claude/ralphie.md +576 -0
- package/templates/.claude/settings.json.example +25 -0
- package/templates/.claude/skills/create-spec/SKILL.md +222 -0
- package/templates/.claude/skills/ralphie-iterate/SKILL.md +959 -0
- package/templates/RALPHIE.md +100 -0
- package/templates/STATE.txt +2 -0
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
|
+
};
|