wiggum-cli 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.js +7 -6
  2. package/dist/tui/app.d.ts +12 -22
  3. package/dist/tui/app.js +130 -314
  4. package/dist/tui/components/AppShell.d.ts +47 -0
  5. package/dist/tui/components/AppShell.js +19 -0
  6. package/dist/tui/components/FooterStatusBar.js +2 -3
  7. package/dist/tui/components/HeaderContent.d.ts +28 -0
  8. package/dist/tui/components/HeaderContent.js +16 -0
  9. package/dist/tui/components/MessageList.d.ts +9 -7
  10. package/dist/tui/components/MessageList.js +23 -17
  11. package/dist/tui/components/RunCompletionSummary.d.ts +22 -0
  12. package/dist/tui/components/RunCompletionSummary.js +23 -0
  13. package/dist/tui/components/SpecCompletionSummary.d.ts +47 -0
  14. package/dist/tui/components/SpecCompletionSummary.js +124 -0
  15. package/dist/tui/components/TipsBar.d.ts +24 -0
  16. package/dist/tui/components/TipsBar.js +23 -0
  17. package/dist/tui/components/WiggumBanner.js +8 -3
  18. package/dist/tui/hooks/useBackgroundRuns.d.ts +52 -0
  19. package/dist/tui/hooks/useBackgroundRuns.js +121 -0
  20. package/dist/tui/orchestration/interview-orchestrator.js +1 -1
  21. package/dist/tui/screens/InitScreen.d.ts +13 -8
  22. package/dist/tui/screens/InitScreen.js +86 -87
  23. package/dist/tui/screens/InterviewScreen.d.ts +11 -8
  24. package/dist/tui/screens/InterviewScreen.js +145 -99
  25. package/dist/tui/screens/MainShell.d.ts +13 -12
  26. package/dist/tui/screens/MainShell.js +65 -69
  27. package/dist/tui/screens/RunScreen.d.ts +17 -1
  28. package/dist/tui/screens/RunScreen.js +235 -80
  29. package/dist/tui/screens/index.d.ts +0 -2
  30. package/dist/tui/screens/index.js +0 -1
  31. package/dist/tui/utils/loop-status.d.ts +22 -3
  32. package/dist/tui/utils/loop-status.js +65 -15
  33. package/package.json +5 -1
  34. package/dist/tui/screens/WelcomeScreen.d.ts +0 -44
  35. package/dist/tui/screens/WelcomeScreen.js +0 -54
@@ -3,22 +3,29 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  * RunScreen - TUI screen for running feature loop
4
4
  *
5
5
  * Spawns feature-loop.sh and polls status files for progress.
6
+ * Wrapped in AppShell for consistent layout.
7
+ *
8
+ * Supports two modes:
9
+ * - Foreground: spawns the process and monitors it
10
+ * - Monitor-only: polls status files without spawning (for /monitor)
11
+ *
12
+ * Esc backgrounds the run in foreground mode; in monitor mode, Esc returns to shell.
13
+ * On completion, shows RunCompletionSummary inline.
6
14
  */
7
15
  import { useCallback, useEffect, useRef, useState } from 'react';
8
16
  import { Box, Text, useInput } from 'ink';
9
- import { spawn } from 'node:child_process';
17
+ import { execFileSync, spawn } from 'node:child_process';
10
18
  import { closeSync, existsSync, openSync, readFileSync } from 'node:fs';
11
19
  import { dirname, join } from 'node:path';
12
- import { FooterStatusBar } from '../components/FooterStatusBar.js';
13
20
  import { Confirm } from '../components/Confirm.js';
21
+ import { AppShell } from '../components/AppShell.js';
22
+ import { RunCompletionSummary } from '../components/RunCompletionSummary.js';
14
23
  import { colors, theme } from '../theme.js';
15
- import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, } from '../utils/loop-status.js';
24
+ import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, getLoopLogPath, } from '../utils/loop-status.js';
16
25
  import { loadConfigWithDefaults } from '../../utils/config.js';
26
+ import { logger } from '../../utils/logger.js';
17
27
  const POLL_INTERVAL_MS = 2500;
18
28
  const ERROR_TAIL_LINES = 12;
19
- /**
20
- * Find the feature-loop.sh script.
21
- */
22
29
  function findFeatureLoopScript(projectRoot, scriptsDir) {
23
30
  const localScript = join(projectRoot, scriptsDir, 'feature-loop.sh');
24
31
  if (existsSync(localScript)) {
@@ -34,9 +41,6 @@ function findFeatureLoopScript(projectRoot, scriptsDir) {
34
41
  }
35
42
  return null;
36
43
  }
37
- /**
38
- * Validate that the spec file exists.
39
- */
40
44
  function findSpecFile(projectRoot, feature, specsDir) {
41
45
  const possiblePaths = [
42
46
  join(projectRoot, specsDir, `${feature}.md`),
@@ -50,9 +54,6 @@ function findSpecFile(projectRoot, feature, specsDir) {
50
54
  }
51
55
  return null;
52
56
  }
53
- /**
54
- * Render a simple progress bar.
55
- */
56
57
  function ProgressBar({ percent, width = 18 }) {
57
58
  const safePercent = Math.max(0, Math.min(100, percent));
58
59
  const filled = Math.round((safePercent / 100) * width);
@@ -75,12 +76,20 @@ function readLogTail(logPath, maxLines) {
75
76
  return null;
76
77
  return lines.slice(-maxLines).join('\n');
77
78
  }
78
- catch {
79
- return null;
79
+ catch (err) {
80
+ return `[Unable to read log: ${err instanceof Error ? err.message : String(err)}]`;
80
81
  }
81
82
  }
82
- export function RunScreen({ featureName, projectRoot, sessionState, onComplete, onCancel, }) {
83
- const [status, setStatus] = useState(() => readLoopStatus(featureName));
83
+ export function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly = false, onComplete, onBackground, onCancel, }) {
84
+ const [status, setStatus] = useState(() => {
85
+ try {
86
+ return readLoopStatus(featureName);
87
+ }
88
+ catch (err) {
89
+ logger.error(`Failed to read initial loop status: ${err instanceof Error ? err.message : String(err)}`);
90
+ return { running: false, iteration: 0, maxIterations: 0, phase: 'unknown', tokensInput: 0, tokensOutput: 0 };
91
+ }
92
+ });
84
93
  const [tasks, setTasks] = useState({
85
94
  tasksDone: 0,
86
95
  tasksPending: 0,
@@ -89,18 +98,27 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
89
98
  });
90
99
  const [branch, setBranch] = useState('-');
91
100
  const [error, setError] = useState(null);
92
- const [isStarting, setIsStarting] = useState(true);
101
+ const [isStarting, setIsStarting] = useState(!monitorOnly);
93
102
  const [showConfirm, setShowConfirm] = useState(false);
103
+ const [completionSummary, setCompletionSummary] = useState(null);
94
104
  const childRef = useRef(null);
95
105
  const stopRequestedRef = useRef(false);
96
106
  const isMountedRef = useRef(true);
97
107
  const startTimeRef = useRef(Date.now());
98
108
  const specsDirRef = useRef('.ralph/specs');
109
+ const completionSentRef = useRef(false);
99
110
  const configRootRef = useRef('.ralph');
100
111
  const scriptsDirRef = useRef('.ralph/scripts');
101
112
  const maxIterationsRef = useRef(0);
102
113
  const maxE2eAttemptsRef = useRef(0);
103
114
  useInput((input, key) => {
115
+ // If showing completion summary, Enter or Esc dismisses
116
+ if (completionSummary) {
117
+ if (key.return || key.escape) {
118
+ onComplete(completionSummary);
119
+ }
120
+ return;
121
+ }
104
122
  if (showConfirm)
105
123
  return;
106
124
  if (key.ctrl && input === 'c') {
@@ -108,7 +126,13 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
108
126
  return;
109
127
  }
110
128
  if (key.escape) {
111
- onCancel();
129
+ if (onBackground && !monitorOnly) {
130
+ // Background the run: don't kill the child, just navigate away
131
+ onBackground(featureName);
132
+ }
133
+ else {
134
+ onCancel();
135
+ }
112
136
  }
113
137
  });
114
138
  const refreshStatus = useCallback(async () => {
@@ -123,13 +147,62 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
123
147
  if (!isMountedRef.current)
124
148
  return;
125
149
  setBranch(getGitBranch(projectRoot));
126
- }, [featureName, projectRoot]);
150
+ // In monitor mode, detect completion (only fire once)
151
+ if (monitorOnly && !nextStatus.running && !completionSentRef.current) {
152
+ completionSentRef.current = true;
153
+ const logPath = getLoopLogPath(featureName);
154
+ const finalMarker = `/tmp/ralph-loop-${featureName}.final`;
155
+ const exitCode = existsSync(finalMarker) ? 0 : 1;
156
+ if (exitCode !== 0) {
157
+ logger.warn(`Monitor mode inferred exit code 1 for ${featureName} (no .final marker). This may be a false negative.`);
158
+ }
159
+ const tasksDone = nextTasks.tasksDone + nextTasks.e2eDone;
160
+ const tasksTotal = tasksDone + nextTasks.tasksPending + nextTasks.e2ePending;
161
+ const errorTail = exitCode !== 0 ? readLogTail(logPath, ERROR_TAIL_LINES) || undefined : undefined;
162
+ setCompletionSummary({
163
+ feature: featureName,
164
+ iterations: nextStatus.iteration,
165
+ maxIterations: nextStatus.maxIterations,
166
+ tasksDone,
167
+ tasksTotal,
168
+ tokensInput: nextStatus.tokensInput,
169
+ tokensOutput: nextStatus.tokensOutput,
170
+ exitCode,
171
+ exitCodeInferred: true,
172
+ branch: getGitBranch(projectRoot),
173
+ logPath,
174
+ errorTail,
175
+ });
176
+ }
177
+ }, [featureName, projectRoot, monitorOnly]);
127
178
  const stopLoop = useCallback(() => {
128
179
  stopRequestedRef.current = true;
129
180
  if (childRef.current) {
130
181
  childRef.current.kill('SIGINT');
131
182
  }
132
- }, []);
183
+ else if (monitorOnly) {
184
+ // In monitor mode, find and kill the loop process by pattern
185
+ if (!/^[a-zA-Z0-9_-]+$/.test(featureName)) {
186
+ logger.error(`Refusing to run pkill with unsafe feature name: ${featureName}`);
187
+ setError('Feature name contains invalid characters.');
188
+ return;
189
+ }
190
+ try {
191
+ execFileSync('pkill', ['-INT', '-f', `feature-loop.sh.*${featureName}`]);
192
+ }
193
+ catch (err) {
194
+ // pkill exit code 1 = no matching process (already exited)
195
+ if (err && typeof err === 'object' && 'status' in err && err.status === 1) {
196
+ // Expected — process already exited
197
+ }
198
+ else {
199
+ const reason = err instanceof Error ? err.message : String(err);
200
+ logger.error(`Failed to stop loop process for ${featureName}: ${reason}`);
201
+ setError(`Could not stop the loop process (${reason}). You may need to kill it manually.`);
202
+ }
203
+ }
204
+ }
205
+ }, [monitorOnly, featureName]);
133
206
  const handleConfirm = useCallback((value) => {
134
207
  setShowConfirm(false);
135
208
  if (value) {
@@ -139,6 +212,41 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
139
212
  useEffect(() => {
140
213
  let cancelled = false;
141
214
  let pollTimer = null;
215
+ if (monitorOnly) {
216
+ // Monitor mode: load config for correct specs path, then poll
217
+ const initMonitor = async () => {
218
+ try {
219
+ const config = sessionState.config ?? await loadConfigWithDefaults(projectRoot);
220
+ specsDirRef.current = config.paths.specs;
221
+ }
222
+ catch (err) {
223
+ const reason = err instanceof Error ? err.message : String(err);
224
+ logger.error(`Failed to load config for monitor mode: ${reason}`);
225
+ // Keep default .ralph/specs but warn user
226
+ if (!cancelled)
227
+ setError(`Could not load project config (${reason}). Showing status from default paths.`);
228
+ }
229
+ if (cancelled)
230
+ return;
231
+ setIsStarting(false);
232
+ refreshStatus().catch((err) => {
233
+ logger.warn(`Status refresh failed: ${err instanceof Error ? err.message : String(err)}`);
234
+ });
235
+ pollTimer = setInterval(() => {
236
+ refreshStatus().catch((err) => {
237
+ logger.warn(`Status refresh failed: ${err instanceof Error ? err.message : String(err)}`);
238
+ });
239
+ }, POLL_INTERVAL_MS);
240
+ };
241
+ initMonitor();
242
+ return () => {
243
+ cancelled = true;
244
+ isMountedRef.current = false;
245
+ if (pollTimer)
246
+ clearInterval(pollTimer);
247
+ };
248
+ }
249
+ // Foreground mode: spawn the process
142
250
  const startLoop = async () => {
143
251
  try {
144
252
  if (!/^[a-zA-Z0-9_-]+$/.test(featureName)) {
@@ -170,66 +278,100 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
170
278
  setIsStarting(false);
171
279
  return;
172
280
  }
173
- const logPath = `/tmp/ralph-loop-${featureName}.log`;
281
+ const logPath = getLoopLogPath(featureName);
174
282
  const logFd = openSync(logPath, 'a');
175
- const args = [
176
- featureName,
177
- String(config.loop.maxIterations),
178
- String(config.loop.maxE2eAttempts),
179
- '--review-mode',
180
- reviewMode,
181
- ];
182
- const child = spawn('bash', [scriptPath, ...args], {
183
- cwd: dirname(scriptPath),
184
- stdio: ['ignore', logFd, logFd],
185
- env: {
186
- ...process.env,
187
- RALPH_CONFIG_ROOT: config.paths.root,
188
- RALPH_SPEC_DIR: config.paths.specs,
189
- RALPH_SCRIPTS_DIR: config.paths.scripts,
190
- },
191
- });
192
- childRef.current = child;
193
- startTimeRef.current = Date.now();
194
- setIsStarting(false);
195
- if (stopRequestedRef.current) {
196
- child.kill('SIGINT');
197
- }
198
- pollTimer = setInterval(() => {
199
- refreshStatus();
200
- }, POLL_INTERVAL_MS);
201
- refreshStatus();
202
- child.on('error', (err) => {
203
- if (cancelled)
204
- return;
205
- setError(`Failed to start feature loop: ${err.message}`);
206
- });
207
- child.on('close', async (code) => {
208
- if (cancelled)
209
- return;
210
- if (pollTimer)
211
- clearInterval(pollTimer);
212
- closeSync(logFd);
213
- const latestStatus = readLoopStatus(featureName);
214
- const latestTasks = await parseImplementationPlan(projectRoot, featureName, specsDirRef.current);
215
- const tasksDone = latestTasks.tasksDone + latestTasks.e2eDone;
216
- const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
217
- const exitCode = typeof code === 'number' ? code : 1;
218
- const errorTail = exitCode === 0 ? undefined : readLogTail(logPath, ERROR_TAIL_LINES) || undefined;
219
- onComplete({
220
- feature: featureName,
221
- iterations: latestStatus.iteration,
222
- maxIterations: latestStatus.maxIterations || config.loop.maxIterations,
223
- tasksDone,
224
- tasksTotal,
225
- tokensInput: latestStatus.tokensInput,
226
- tokensOutput: latestStatus.tokensOutput,
227
- exitCode,
228
- branch: getGitBranch(projectRoot),
229
- logPath,
230
- errorTail,
283
+ let logFdClosed = false;
284
+ try {
285
+ const args = [
286
+ featureName,
287
+ String(config.loop.maxIterations),
288
+ String(config.loop.maxE2eAttempts),
289
+ '--review-mode',
290
+ reviewMode,
291
+ ];
292
+ const child = spawn('bash', [scriptPath, ...args], {
293
+ cwd: dirname(scriptPath),
294
+ stdio: ['ignore', logFd, logFd],
295
+ env: {
296
+ ...process.env,
297
+ RALPH_CONFIG_ROOT: config.paths.root,
298
+ RALPH_SPEC_DIR: config.paths.specs,
299
+ RALPH_SCRIPTS_DIR: config.paths.scripts,
300
+ },
231
301
  });
232
- });
302
+ childRef.current = child;
303
+ startTimeRef.current = Date.now();
304
+ setIsStarting(false);
305
+ if (stopRequestedRef.current) {
306
+ child.kill('SIGINT');
307
+ }
308
+ pollTimer = setInterval(() => {
309
+ refreshStatus().catch((err) => {
310
+ logger.warn(`Status refresh failed: ${err instanceof Error ? err.message : String(err)}`);
311
+ });
312
+ }, POLL_INTERVAL_MS);
313
+ refreshStatus().catch((err) => {
314
+ logger.warn(`Status refresh failed: ${err instanceof Error ? err.message : String(err)}`);
315
+ });
316
+ child.on('error', (err) => {
317
+ if (!logFdClosed) {
318
+ closeSync(logFd);
319
+ logFdClosed = true;
320
+ }
321
+ if (cancelled)
322
+ return;
323
+ setError(`Failed to start feature loop: ${err.message}`);
324
+ });
325
+ child.on('close', async (code) => {
326
+ if (cancelled)
327
+ return;
328
+ if (pollTimer)
329
+ clearInterval(pollTimer);
330
+ if (!logFdClosed) {
331
+ closeSync(logFd);
332
+ logFdClosed = true;
333
+ }
334
+ if (!isMountedRef.current)
335
+ return;
336
+ let latestStatus;
337
+ let latestTasks;
338
+ try {
339
+ latestStatus = readLoopStatus(featureName);
340
+ latestTasks = await parseImplementationPlan(projectRoot, featureName, specsDirRef.current);
341
+ }
342
+ catch (err) {
343
+ logger.error(`Failed to read final run status for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
344
+ latestStatus = { running: false, iteration: 0, maxIterations: config.loop.maxIterations, phase: 'unknown', tokensInput: 0, tokensOutput: 0 };
345
+ latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0 };
346
+ }
347
+ const tasksDone = latestTasks.tasksDone + latestTasks.e2eDone;
348
+ const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
349
+ const exitCode = typeof code === 'number' ? code : 1;
350
+ const errorTail = exitCode === 0 ? undefined : readLogTail(logPath, ERROR_TAIL_LINES) || undefined;
351
+ const summary = {
352
+ feature: featureName,
353
+ iterations: latestStatus.iteration,
354
+ maxIterations: latestStatus.maxIterations || config.loop.maxIterations,
355
+ tasksDone,
356
+ tasksTotal,
357
+ tokensInput: latestStatus.tokensInput,
358
+ tokensOutput: latestStatus.tokensOutput,
359
+ exitCode,
360
+ branch: getGitBranch(projectRoot),
361
+ logPath,
362
+ errorTail,
363
+ };
364
+ // Show completion summary inline
365
+ setCompletionSummary(summary);
366
+ });
367
+ }
368
+ catch (spawnErr) {
369
+ if (!logFdClosed) {
370
+ closeSync(logFd);
371
+ logFdClosed = true;
372
+ }
373
+ throw spawnErr;
374
+ }
233
375
  }
234
376
  catch (err) {
235
377
  if (cancelled)
@@ -245,7 +387,7 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
245
387
  if (pollTimer)
246
388
  clearInterval(pollTimer);
247
389
  };
248
- }, [featureName, projectRoot, refreshStatus, onComplete, sessionState.config]);
390
+ }, [featureName, projectRoot, refreshStatus, monitorOnly, sessionState.config]);
249
391
  const totalTasks = tasks.tasksDone + tasks.tasksPending;
250
392
  const totalE2e = tasks.e2eDone + tasks.e2ePending;
251
393
  const totalAll = totalTasks + totalE2e;
@@ -255,5 +397,18 @@ export function RunScreen({ featureName, projectRoot, sessionState, onComplete,
255
397
  const percentAll = totalAll > 0 ? Math.round((doneAll / totalAll) * 100) : 0;
256
398
  const totalTokens = status.tokensInput + status.tokensOutput;
257
399
  const phaseLine = isStarting ? 'Starting...' : status.phase;
258
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.error, children: ["Error: ", error] }) })), !error && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: phaseLine }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: status.iteration }), _jsxs(Text, { dimColor: true, children: ["/", status.maxIterations || maxIterationsRef.current || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(status.tokensInput), " out:", formatNumber(status.tokensOutput), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTimeRef.current)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: "Implementation:" }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [percentTasks, "%"] }), _jsxs(Text, { color: colors.green, children: ["\u2713 ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ["\u25CB ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: "E2E Tests:" }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [percentE2e, "%"] }), _jsxs(Text, { color: colors.green, children: ["\u2713 ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ["\u25CB ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Overall:" }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [percentAll, "%"] }), _jsxs(Text, { color: colors.green, children: ["\u2713 ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ["\u25CB ", totalAll - doneAll] })] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Tip: /monitor ", featureName, " in another terminal for details"] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop the loop" }) })] })), showConfirm && (_jsx(Box, { marginTop: 1, children: _jsx(Confirm, { message: stopRequestedRef.current ? 'Stopping loop...' : 'Stop the feature loop?', onConfirm: handleConfirm, onCancel: () => setShowConfirm(false), initialValue: false }) })), _jsx(FooterStatusBar, { action: "Run Loop", phase: phaseLine, path: featureName })] }));
400
+ const isRunning = !completionSummary && !error;
401
+ // Tips text
402
+ const tips = completionSummary
403
+ ? 'Enter to return to shell'
404
+ : monitorOnly
405
+ ? 'Ctrl+C stop, Esc back'
406
+ : 'Ctrl+C stop, Esc background';
407
+ // Input element (only show Confirm when stopping)
408
+ const inputElement = showConfirm ? (_jsx(Confirm, { message: stopRequestedRef.current ? 'Stopping loop...' : 'Stop the feature loop?', onConfirm: handleConfirm, onCancel: () => setShowConfirm(false), initialValue: false })) : null;
409
+ return (_jsx(AppShell, { header: header, tips: tips, isWorking: isRunning && !isStarting, workingStatus: `${phaseLine} \u2014 ${featureName}`, workingHint: monitorOnly ? 'esc to go back' : 'esc to background', input: inputElement, error: error, footerStatus: {
410
+ action: 'Run Loop',
411
+ phase: phaseLine,
412
+ path: featureName,
413
+ }, children: completionSummary ? (_jsx(RunCompletionSummary, { summary: completionSummary })) : (!error && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: phaseLine }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: status.iteration }), _jsxs(Text, { dimColor: true, children: ["/", status.maxIterations || maxIterationsRef.current || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(status.tokensInput), " out:", formatNumber(status.tokensOutput), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTimeRef.current)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: "Implementation:" }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [percentTasks, "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: "E2E Tests:" }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [percentE2e, "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Overall:" }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [percentAll, "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] })] }))) }));
259
414
  }
@@ -5,8 +5,6 @@ export { InterviewScreen } from './InterviewScreen.js';
5
5
  export type { InterviewScreenProps } from './InterviewScreen.js';
6
6
  export { InitScreen } from './InitScreen.js';
7
7
  export type { InitScreenProps } from './InitScreen.js';
8
- export { WelcomeScreen } from './WelcomeScreen.js';
9
- export type { WelcomeScreenProps } from './WelcomeScreen.js';
10
8
  export { MainShell } from './MainShell.js';
11
9
  export type { MainShellProps, NavigationTarget, NavigationProps } from './MainShell.js';
12
10
  export { RunScreen } from './RunScreen.js';
@@ -3,6 +3,5 @@
3
3
  */
4
4
  export { InterviewScreen } from './InterviewScreen.js';
5
5
  export { InitScreen } from './InitScreen.js';
6
- export { WelcomeScreen } from './WelcomeScreen.js';
7
6
  export { MainShell } from './MainShell.js';
8
7
  export { RunScreen } from './RunScreen.js';
@@ -16,15 +16,34 @@ export interface TaskCounts {
16
16
  e2ePending: number;
17
17
  }
18
18
  /**
19
- * Detect current phase of the loop based on active prompt files.
19
+ * Return the conventional log file path for a feature loop.
20
+ */
21
+ export declare function getLoopLogPath(feature: string): string;
22
+ /**
23
+ * Detect current phase of the loop by checking for processes with prompt file patterns in their command line.
24
+ *
25
+ * Note: prompt-file checks (PROMPT_feature.md, etc.) are global — they match any
26
+ * running process, not just the one for `feature`. This is acceptable because
27
+ * concurrent loops are rare, but callers should be aware of the limitation.
20
28
  */
21
29
  export declare function detectPhase(feature: string): string;
22
30
  /**
23
- * Read status from temp files.
31
+ * Read loop status from temp files written by feature-loop.sh.
32
+ *
33
+ * Reads `ralph-loop-<feature>.status` (or `.final`) for iteration progress
34
+ * and `ralph-loop-<feature>.tokens` for token counts. Also runs `pgrep` to
35
+ * check whether the loop process is still alive.
36
+ *
37
+ * @throws {Error} If `feature` contains invalid characters.
24
38
  */
25
39
  export declare function readLoopStatus(feature: string): LoopStatus;
26
40
  /**
27
- * Parse implementation plan for task counts.
41
+ * Parse the markdown implementation plan for a feature to extract task/E2E counts.
42
+ *
43
+ * Looks for `- [x]` (done) and `- [ ]` (pending) checklist items.
44
+ * Items containing "E2E:" are counted separately as end-to-end tests.
45
+ *
46
+ * @returns Counts of done/pending tasks and E2E tests.
28
47
  */
29
48
  export declare function parseImplementationPlan(projectRoot: string, feature: string, specsDirOverride?: string): Promise<TaskCounts>;
30
49
  /**
@@ -5,20 +5,49 @@ import { execFileSync } from 'node:child_process';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { loadConfigWithDefaults } from '../../utils/config.js';
8
+ import { logger } from '../../utils/logger.js';
9
+ /**
10
+ * Track whether pgrep is available to avoid repeated failed calls.
11
+ * null = untested, true = available, false = unavailable
12
+ */
13
+ let pgrepAvailable = null;
8
14
  /**
9
15
  * Check if a process matching pattern is running.
10
16
  */
11
17
  function isProcessRunning(pattern) {
18
+ if (pgrepAvailable === false)
19
+ return false;
12
20
  try {
13
21
  const result = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' });
22
+ pgrepAvailable = true;
14
23
  return result.trim().length > 0;
15
24
  }
16
- catch {
25
+ catch (err) {
26
+ // pgrep exits with code 1 when no processes match — that's expected
27
+ if (err && typeof err === 'object' && 'status' in err && err.status === 1) {
28
+ pgrepAvailable = true;
29
+ return false;
30
+ }
31
+ // Any other error (pgrep not installed, permission denied, etc.)
32
+ if (pgrepAvailable === null) {
33
+ logger.warn(`Process detection unavailable: ${err instanceof Error ? err.message : String(err)}. Background run status may be inaccurate.`);
34
+ pgrepAvailable = false;
35
+ }
17
36
  return false;
18
37
  }
19
38
  }
20
39
  /**
21
- * Detect current phase of the loop based on active prompt files.
40
+ * Return the conventional log file path for a feature loop.
41
+ */
42
+ export function getLoopLogPath(feature) {
43
+ return `/tmp/ralph-loop-${feature}.log`;
44
+ }
45
+ /**
46
+ * Detect current phase of the loop by checking for processes with prompt file patterns in their command line.
47
+ *
48
+ * Note: prompt-file checks (PROMPT_feature.md, etc.) are global — they match any
49
+ * running process, not just the one for `feature`. This is acceptable because
50
+ * concurrent loops are rare, but callers should be aware of the limitation.
22
51
  */
23
52
  export function detectPhase(feature) {
24
53
  if (isProcessRunning('PROMPT_feature.md'))
@@ -38,9 +67,18 @@ export function detectPhase(feature) {
38
67
  return 'Idle';
39
68
  }
40
69
  /**
41
- * Read status from temp files.
70
+ * Read loop status from temp files written by feature-loop.sh.
71
+ *
72
+ * Reads `ralph-loop-<feature>.status` (or `.final`) for iteration progress
73
+ * and `ralph-loop-<feature>.tokens` for token counts. Also runs `pgrep` to
74
+ * check whether the loop process is still alive.
75
+ *
76
+ * @throws {Error} If `feature` contains invalid characters.
42
77
  */
43
78
  export function readLoopStatus(feature) {
79
+ if (!/^[a-zA-Z0-9_-]+$/.test(feature)) {
80
+ throw new Error(`Invalid feature name: "${feature}". Must contain only letters, numbers, hyphens, and underscores.`);
81
+ }
44
82
  const statusFile = `/tmp/ralph-loop-${feature}.status`;
45
83
  const finalStatusFile = `/tmp/ralph-loop-${feature}.final`;
46
84
  const tokensFile = `/tmp/ralph-loop-${feature}.tokens`;
@@ -54,8 +92,8 @@ export function readLoopStatus(feature) {
54
92
  iteration = parseInt(parts[0] || '0', 10) || 0;
55
93
  maxIterations = parseInt(parts[1] || '0', 10) || 0;
56
94
  }
57
- catch {
58
- // Ignore parse errors
95
+ catch (err) {
96
+ logger.debug(`Failed to parse status file: ${err instanceof Error ? err.message : String(err)}`);
59
97
  }
60
98
  }
61
99
  let tokensInput = 0;
@@ -67,8 +105,8 @@ export function readLoopStatus(feature) {
67
105
  tokensInput = parseInt(parts[0] || '0', 10) || 0;
68
106
  tokensOutput = parseInt(parts[1] || '0', 10) || 0;
69
107
  }
70
- catch {
71
- // Ignore parse errors
108
+ catch (err) {
109
+ logger.debug(`Failed to parse tokens file: ${err instanceof Error ? err.message : String(err)}`);
72
110
  }
73
111
  }
74
112
  return {
@@ -81,12 +119,23 @@ export function readLoopStatus(feature) {
81
119
  };
82
120
  }
83
121
  /**
84
- * Parse implementation plan for task counts.
122
+ * Parse the markdown implementation plan for a feature to extract task/E2E counts.
123
+ *
124
+ * Looks for `- [x]` (done) and `- [ ]` (pending) checklist items.
125
+ * Items containing "E2E:" are counted separately as end-to-end tests.
126
+ *
127
+ * @returns Counts of done/pending tasks and E2E tests.
85
128
  */
86
129
  export async function parseImplementationPlan(projectRoot, feature, specsDirOverride) {
87
- const config = specsDirOverride
88
- ? null
89
- : await loadConfigWithDefaults(projectRoot);
130
+ let config = null;
131
+ if (!specsDirOverride) {
132
+ try {
133
+ config = await loadConfigWithDefaults(projectRoot);
134
+ }
135
+ catch (err) {
136
+ logger.debug(`Failed to load config for plan parsing: ${err instanceof Error ? err.message : String(err)}`);
137
+ }
138
+ }
90
139
  const specsDir = specsDirOverride || config?.paths.specs || '.ralph/specs';
91
140
  const planPath = join(projectRoot, specsDir, `${feature}-implementation-plan.md`);
92
141
  let tasksDone = 0;
@@ -117,8 +166,8 @@ export async function parseImplementationPlan(projectRoot, feature, specsDirOver
117
166
  }
118
167
  }
119
168
  }
120
- catch {
121
- // Ignore parse errors
169
+ catch (err) {
170
+ logger.debug(`Failed to parse implementation plan: ${err instanceof Error ? err.message : String(err)}`);
122
171
  }
123
172
  }
124
173
  return { tasksDone, tasksPending, e2eDone, e2ePending };
@@ -131,9 +180,10 @@ export function getGitBranch(projectRoot) {
131
180
  return execFileSync('git', ['branch', '--show-current'], {
132
181
  cwd: projectRoot,
133
182
  encoding: 'utf-8',
134
- }).trim();
183
+ }).trim() || '(detached HEAD)';
135
184
  }
136
- catch {
185
+ catch (err) {
186
+ logger.debug(`getGitBranch failed: ${err instanceof Error ? err.message : String(err)}`);
137
187
  return '-';
138
188
  }
139
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiggum-cli",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "AI-powered feature development loop CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,10 @@
33
33
  "code-generation",
34
34
  "tech-stack-detection"
35
35
  ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/federiconeri/wiggum-cli"
39
+ },
36
40
  "author": "Wiggum CLI Contributors",
37
41
  "license": "SEE LICENSE IN LICENSE",
38
42
  "dependencies": {