wiggum-cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +7 -1
  2. package/bin/ralph.js +0 -0
  3. package/dist/agent/memory/ingest.d.ts +14 -0
  4. package/dist/agent/memory/ingest.js +77 -0
  5. package/dist/agent/memory/store.d.ts +15 -0
  6. package/dist/agent/memory/store.js +98 -0
  7. package/dist/agent/memory/types.d.ts +16 -0
  8. package/dist/agent/memory/types.js +14 -0
  9. package/dist/agent/orchestrator.d.ts +7 -0
  10. package/dist/agent/orchestrator.js +266 -0
  11. package/dist/agent/resolve-config.d.ts +26 -0
  12. package/dist/agent/resolve-config.js +43 -0
  13. package/dist/agent/tools/backlog.d.ts +27 -0
  14. package/dist/agent/tools/backlog.js +51 -0
  15. package/dist/agent/tools/dry-run.d.ts +106 -0
  16. package/dist/agent/tools/dry-run.js +119 -0
  17. package/dist/agent/tools/execution.d.ts +51 -0
  18. package/dist/agent/tools/execution.js +256 -0
  19. package/dist/agent/tools/feature-state.d.ts +43 -0
  20. package/dist/agent/tools/feature-state.js +184 -0
  21. package/dist/agent/tools/introspection.d.ts +23 -0
  22. package/dist/agent/tools/introspection.js +40 -0
  23. package/dist/agent/tools/memory.d.ts +44 -0
  24. package/dist/agent/tools/memory.js +99 -0
  25. package/dist/agent/tools/preflight.d.ts +7 -0
  26. package/dist/agent/tools/preflight.js +137 -0
  27. package/dist/agent/tools/reporting.d.ts +58 -0
  28. package/dist/agent/tools/reporting.js +119 -0
  29. package/dist/agent/tools/schemas.d.ts +2 -0
  30. package/dist/agent/tools/schemas.js +3 -0
  31. package/dist/agent/types.d.ts +45 -0
  32. package/dist/agent/types.js +1 -0
  33. package/dist/ai/conversation/conversation-manager.js +8 -0
  34. package/dist/ai/conversation/url-fetcher.js +27 -0
  35. package/dist/ai/providers.js +5 -5
  36. package/dist/commands/agent.d.ts +17 -0
  37. package/dist/commands/agent.js +114 -0
  38. package/dist/commands/monitor.js +50 -183
  39. package/dist/commands/new-auto.d.ts +15 -0
  40. package/dist/commands/new-auto.js +237 -0
  41. package/dist/commands/run.js +20 -10
  42. package/dist/commands/sync.d.ts +15 -0
  43. package/dist/commands/sync.js +68 -0
  44. package/dist/generator/config.d.ts +1 -41
  45. package/dist/generator/config.js +7 -0
  46. package/dist/generator/index.d.ts +2 -2
  47. package/dist/generator/templates.d.ts +3 -0
  48. package/dist/generator/templates.js +22 -1
  49. package/dist/index.d.ts +14 -1
  50. package/dist/index.js +333 -40
  51. package/dist/repl/command-parser.d.ts +5 -0
  52. package/dist/repl/command-parser.js +5 -0
  53. package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
  54. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
  55. package/dist/templates/prompts/PROMPT_feature.md.tmpl +39 -3
  56. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
  57. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  58. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
  59. package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  60. package/dist/templates/scripts/feature-loop.sh.tmpl +611 -95
  61. package/dist/tui/app.d.ts +34 -2
  62. package/dist/tui/app.js +31 -5
  63. package/dist/tui/components/ActivityFeed.d.ts +18 -0
  64. package/dist/tui/components/ActivityFeed.js +31 -0
  65. package/dist/tui/components/IssuePicker.d.ts +27 -0
  66. package/dist/tui/components/IssuePicker.js +64 -0
  67. package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
  68. package/dist/tui/components/RunCompletionSummary.js +103 -10
  69. package/dist/tui/components/SummaryBox.d.ts +4 -0
  70. package/dist/tui/components/SummaryBox.js +4 -2
  71. package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
  72. package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
  73. package/dist/tui/hooks/useBackgroundRuns.js +1 -1
  74. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  75. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  76. package/dist/tui/screens/AgentScreen.d.ts +21 -0
  77. package/dist/tui/screens/AgentScreen.js +159 -0
  78. package/dist/tui/screens/InitScreen.js +4 -0
  79. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  80. package/dist/tui/screens/InterviewScreen.js +146 -10
  81. package/dist/tui/screens/MainShell.d.ts +1 -1
  82. package/dist/tui/screens/MainShell.js +36 -1
  83. package/dist/tui/screens/RunScreen.d.ts +15 -15
  84. package/dist/tui/screens/RunScreen.js +96 -11
  85. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  86. package/dist/tui/utils/build-run-summary.js +44 -85
  87. package/dist/tui/utils/clear-screen.d.ts +14 -0
  88. package/dist/tui/utils/clear-screen.js +16 -0
  89. package/dist/tui/utils/git-summary.d.ts +13 -0
  90. package/dist/tui/utils/git-summary.js +30 -0
  91. package/dist/tui/utils/loop-status.d.ts +94 -0
  92. package/dist/tui/utils/loop-status.js +430 -10
  93. package/dist/tui/utils/pr-summary.d.ts +3 -2
  94. package/dist/tui/utils/pr-summary.js +41 -6
  95. package/dist/utils/ci.d.ts +8 -0
  96. package/dist/utils/ci.js +13 -0
  97. package/dist/utils/config.d.ts +8 -0
  98. package/dist/utils/config.js +8 -0
  99. package/dist/utils/github.d.ts +32 -0
  100. package/dist/utils/github.js +106 -0
  101. package/dist/utils/spec-names.js +5 -1
  102. package/package.json +10 -2
  103. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  104. package/src/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
  105. package/src/templates/prompts/PROMPT_feature.md.tmpl +39 -3
  106. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
  107. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  108. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
  109. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  110. package/src/templates/scripts/feature-loop.sh.tmpl +611 -95
@@ -21,9 +21,11 @@ import { Confirm } from '../components/Confirm.js';
21
21
  import { Select } from '../components/Select.js';
22
22
  import { AppShell } from '../components/AppShell.js';
23
23
  import { RunCompletionSummary } from '../components/RunCompletionSummary.js';
24
+ import { ActivityFeed } from '../components/ActivityFeed.js';
24
25
  import { colors, theme } from '../theme.js';
25
- import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, getLoopLogPath, } from '../utils/loop-status.js';
26
+ import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, getLoopLogPath, parseLoopLog, parsePhaseChanges, } from '../utils/loop-status.js';
26
27
  import { buildEnhancedRunSummary } from '../utils/build-run-summary.js';
28
+ import { getCurrentCommitHash } from '../utils/git-summary.js';
27
29
  import { writeRunSummaryFile } from '../../utils/summary-file.js';
28
30
  import { loadConfigWithDefaults } from '../../utils/config.js';
29
31
  import { logger } from '../../utils/logger.js';
@@ -91,7 +93,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
91
93
  }
92
94
  catch (err) {
93
95
  logger.error(`Failed to read initial loop status: ${err instanceof Error ? err.message : String(err)}`);
94
- return { running: false, iteration: 0, maxIterations: 0, phase: 'unknown', tokensInput: 0, tokensOutput: 0 };
96
+ return { running: false, iteration: 0, maxIterations: 0, phase: 'unknown', tokensInput: 0, tokensOutput: 0, cacheCreate: 0, cacheRead: 0 };
95
97
  }
96
98
  });
97
99
  const [tasks, setTasks] = useState({
@@ -99,6 +101,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
99
101
  tasksPending: 0,
100
102
  e2eDone: 0,
101
103
  e2ePending: 0,
104
+ planExists: false,
102
105
  });
103
106
  const [branch, setBranch] = useState('-');
104
107
  const [error, setError] = useState(null);
@@ -106,6 +109,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
106
109
  const [showConfirm, setShowConfirm] = useState(false);
107
110
  const [completionSummary, setCompletionSummary] = useState(null);
108
111
  const [actionRequest, setActionRequest] = useState(null);
112
+ const [activityEvents, setActivityEvents] = useState([]);
113
+ const [latestCommit, setLatestCommit] = useState(null);
114
+ const [baselineCommit, setBaselineCommit] = useState(null);
109
115
  const childRef = useRef(null);
110
116
  const stopRequestedRef = useRef(false);
111
117
  const isMountedRef = useRef(true);
@@ -117,6 +123,28 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
117
123
  const maxIterationsRef = useRef(0);
118
124
  const maxE2eAttemptsRef = useRef(0);
119
125
  const handledActionIdRef = useRef(null);
126
+ const lastLogLineCountRef = useRef(0);
127
+ const lastKnownPhasesRef = useRef(undefined);
128
+ const lastActivityTimeRef = useRef(Date.now());
129
+ const lastCommitForEventRef = useRef(null);
130
+ // Read baseline commit once on mount
131
+ useEffect(() => {
132
+ const baselinePath = `/tmp/ralph-loop-${featureName}.baseline`;
133
+ try {
134
+ if (existsSync(baselinePath)) {
135
+ const hash = readFileSync(baselinePath, 'utf-8').trim();
136
+ if (hash)
137
+ setBaselineCommit(hash);
138
+ }
139
+ }
140
+ catch {
141
+ // Baseline file is optional — ignore read errors
142
+ }
143
+ // Also capture current HEAD as initial latestCommit
144
+ const head = getCurrentCommitHash(projectRoot);
145
+ if (head)
146
+ setLatestCommit(head);
147
+ }, [featureName, projectRoot]);
120
148
  useInput((input, key) => {
121
149
  // If showing completion summary, Enter or Esc dismisses
122
150
  if (completionSummary) {
@@ -156,6 +184,50 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
156
184
  if (!isMountedRef.current)
157
185
  return;
158
186
  setBranch(getGitBranch(projectRoot));
187
+ // Update latest commit hash
188
+ const head = getCurrentCommitHash(projectRoot);
189
+ if (head && isMountedRef.current)
190
+ setLatestCommit(head);
191
+ // Collect new activity events from log and phase changes
192
+ const logPath = getLoopLogPath(featureName);
193
+ const allLogEvents = parseLoopLog(logPath);
194
+ // Detect log file truncation/rotation and reset tracking
195
+ if (allLogEvents.length < lastLogLineCountRef.current) {
196
+ lastLogLineCountRef.current = 0;
197
+ }
198
+ const newLogEvents = allLogEvents.slice(lastLogLineCountRef.current);
199
+ lastLogLineCountRef.current = allLogEvents.length;
200
+ const { events: phaseEvents, currentPhases } = parsePhaseChanges(featureName, lastKnownPhasesRef.current);
201
+ if (currentPhases) {
202
+ lastKnownPhasesRef.current = currentPhases;
203
+ }
204
+ // Emit a commit activity event when HEAD changes
205
+ if (head && head !== lastCommitForEventRef.current && lastCommitForEventRef.current !== null) {
206
+ newLogEvents.push({
207
+ timestamp: Date.now(),
208
+ message: `New commit: ${head.slice(0, 7)}`,
209
+ status: 'success',
210
+ });
211
+ }
212
+ lastCommitForEventRef.current = head ?? null;
213
+ const MAX_STORED_EVENTS = 100;
214
+ const newEvents = [...newLogEvents, ...phaseEvents];
215
+ if (newEvents.length > 0 && isMountedRef.current) {
216
+ lastActivityTimeRef.current = Date.now();
217
+ setActivityEvents((prev) => [...prev, ...newEvents].slice(-MAX_STORED_EVENTS));
218
+ }
219
+ else if (nextStatus.running &&
220
+ nextStatus.phase !== 'Idle' &&
221
+ Date.now() - lastActivityTimeRef.current > 30_000 &&
222
+ isMountedRef.current) {
223
+ // Inject a synthetic "session in progress" event when stale
224
+ // Update lastActivityTimeRef so this doesn't fire every poll cycle
225
+ lastActivityTimeRef.current = Date.now();
226
+ setActivityEvents((prev) => [
227
+ ...prev,
228
+ { timestamp: Date.now(), message: `${nextStatus.phase} session in progress...`, status: 'in-progress' },
229
+ ].slice(-MAX_STORED_EVENTS));
230
+ }
159
231
  // Check for pending action request (loop waiting for user input)
160
232
  const request = readActionRequest(featureName);
161
233
  if (!isMountedRef.current)
@@ -183,6 +255,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
183
255
  const tasksDone = nextTasks.tasksDone + nextTasks.e2eDone;
184
256
  const tasksTotal = tasksDone + nextTasks.tasksPending + nextTasks.e2ePending;
185
257
  const errorTail = exitCode !== 0 ? readLogTail(logPath, ERROR_TAIL_LINES) || undefined : undefined;
258
+ // Use feat/<feature> as the branch name for summary. getGitBranch() returns
259
+ // "main" after squash-merge + worktree cleanup, which breaks PR/issue detection.
260
+ const summaryBranch = `feat/${featureName}`;
186
261
  const basicSummary = {
187
262
  feature: featureName,
188
263
  iterations: nextStatus.iteration,
@@ -191,15 +266,17 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
191
266
  tasksTotal,
192
267
  tokensInput: nextStatus.tokensInput,
193
268
  tokensOutput: nextStatus.tokensOutput,
269
+ cacheCreate: nextStatus.cacheCreate,
270
+ cacheRead: nextStatus.cacheRead,
194
271
  exitCode,
195
272
  exitCodeInferred: true,
196
- branch: getGitBranch(projectRoot),
273
+ branch: summaryBranch,
197
274
  logPath,
198
275
  errorTail,
199
276
  };
200
277
  let enhancedSummary;
201
278
  try {
202
- enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
279
+ enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
203
280
  }
204
281
  catch (err) {
205
282
  logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
@@ -211,7 +288,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
211
288
  logger.error(`Failed to persist summary file for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
212
289
  });
213
290
  }
214
- }, [featureName, projectRoot, monitorOnly]);
291
+ }, [featureName, projectRoot, monitorOnly, baselineCommit]);
215
292
  // Keep a stable ref to the latest refreshStatus so the spawn effect
216
293
  // can schedule polls without re-running when refreshStatus changes.
217
294
  const refreshStatusRef = useRef(refreshStatus);
@@ -376,6 +453,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
376
453
  closeSync(logFd);
377
454
  logFdClosed = true;
378
455
  }
456
+ if (!isMountedRef.current)
457
+ return;
458
+ // Wait for bash to flush state files (.phases, .tokens, .final)
459
+ await new Promise((r) => setTimeout(r, 200));
379
460
  if (!isMountedRef.current)
380
461
  return;
381
462
  let latestStatus;
@@ -386,8 +467,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
386
467
  }
387
468
  catch (err) {
388
469
  logger.error(`Failed to read final run status for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
389
- latestStatus = { running: false, iteration: 0, maxIterations: config.loop.maxIterations, phase: 'unknown', tokensInput: 0, tokensOutput: 0 };
390
- latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0 };
470
+ latestStatus = { running: false, iteration: 0, maxIterations: config.loop.maxIterations, phase: 'unknown', tokensInput: 0, tokensOutput: 0, cacheCreate: 0, cacheRead: 0 };
471
+ latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0, planExists: false };
391
472
  }
392
473
  const tasksDone = latestTasks.tasksDone + latestTasks.e2eDone;
393
474
  const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
@@ -401,15 +482,17 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
401
482
  tasksTotal,
402
483
  tokensInput: latestStatus.tokensInput,
403
484
  tokensOutput: latestStatus.tokensOutput,
485
+ cacheCreate: latestStatus.cacheCreate,
486
+ cacheRead: latestStatus.cacheRead,
404
487
  exitCode,
405
- branch: getGitBranch(projectRoot),
488
+ branch: `feat/${featureName}`,
406
489
  logPath,
407
490
  errorTail,
408
491
  };
409
492
  // Build enhanced summary with phases, git stats, PR/issue metadata
410
493
  let enhancedSummary;
411
494
  try {
412
- enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
495
+ enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
413
496
  }
414
497
  catch (err) {
415
498
  logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
@@ -456,7 +539,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
456
539
  const percentTasks = totalTasks > 0 ? Math.round((tasks.tasksDone / totalTasks) * 100) : 0;
457
540
  const percentE2e = totalE2e > 0 ? Math.round((tasks.e2eDone / totalE2e) * 100) : 0;
458
541
  const percentAll = totalAll > 0 ? Math.round((doneAll / totalAll) * 100) : 0;
459
- const totalTokens = status.tokensInput + status.tokensOutput;
542
+ const totalTokens = status.tokensInput + status.tokensOutput + status.cacheCreate + status.cacheRead;
460
543
  const phaseLine = isStarting ? 'Starting...' : status.phase;
461
544
  const isRunning = !completionSummary && !error;
462
545
  // Tips text
@@ -507,5 +590,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
507
590
  action: 'Run Loop',
508
591
  phase: phaseLine,
509
592
  path: featureName,
510
- }, 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] })] })] })] }))) }));
593
+ }, 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), " cache:", formatNumber(status.cacheRead), ")"] }), _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] })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, latestCommit: baselineCommit && latestCommit && baselineCommit !== latestCommit
594
+ ? `${baselineCommit} \u2192 ${latestCommit}`
595
+ : latestCommit || undefined })] })] }))) }));
511
596
  }
@@ -21,4 +21,4 @@ import type { RunSummary } from '../screens/RunScreen.js';
21
21
  * which blocks the event loop. Callers should wrap in try-catch to handle
22
22
  * failures gracefully.
23
23
  */
24
- export declare function buildEnhancedRunSummary(basicSummary: RunSummary, projectRoot: string, feature: string): RunSummary;
24
+ export declare function buildEnhancedRunSummary(basicSummary: RunSummary, projectRoot: string, feature: string, baselineOverride?: string | null): RunSummary;
@@ -3,86 +3,10 @@
3
3
  */
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { logger } from '../../utils/logger.js';
6
- import { getCurrentCommitHash, getDiffStats } from './git-summary.js';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { getCurrentCommitHash, getDiffStats, getCommitList } from './git-summary.js';
7
8
  import { getPrForBranch, getLinkedIssue } from './pr-summary.js';
8
- /**
9
- * Phase ID to human-readable label mapping
10
- */
11
- const PHASE_LABELS = {
12
- planning: 'Planning',
13
- implementation: 'Implementation',
14
- e2e_testing: 'E2E Testing',
15
- verification: 'Verification',
16
- pr_review: 'PR & Review',
17
- };
18
- const VALID_PHASE_STATUSES = new Set(['success', 'skipped', 'failed']);
19
- /**
20
- * Parse phase information from the phases file written by feature-loop.sh.
21
- *
22
- * Format: phase_id|status|start_timestamp|end_timestamp
23
- * The parser handles duplicate phase entries defensively (last status wins,
24
- * durations aggregate), though feature-loop.sh normally writes one final
25
- * line per phase.
26
- *
27
- * @param phasesFilePath - Path to the phases file
28
- * @returns Array of phase info objects
29
- */
30
- function parsePhases(phasesFilePath) {
31
- if (!existsSync(phasesFilePath)) {
32
- logger.debug(`Phases file not found: ${phasesFilePath}`);
33
- return [];
34
- }
35
- try {
36
- const content = readFileSync(phasesFilePath, 'utf-8').trim();
37
- if (!content) {
38
- return [];
39
- }
40
- const lines = content.split('\n');
41
- const phaseMap = new Map();
42
- for (const line of lines) {
43
- const parts = line.split('|');
44
- if (parts.length < 4) {
45
- logger.warn(`Skipping malformed phase line: ${line}`);
46
- continue;
47
- }
48
- const [id, status, startStr, endStr] = parts;
49
- // Validate status
50
- if (!VALID_PHASE_STATUSES.has(status)) {
51
- logger.warn(`Unknown phase status "${status}" for phase "${id}", treating as failed`);
52
- }
53
- const validatedStatus = VALID_PHASE_STATUSES.has(status)
54
- ? status
55
- : 'failed';
56
- // Parse timestamps
57
- const startTime = parseInt(startStr, 10) || 0;
58
- const endTime = parseInt(endStr, 10) || 0;
59
- // Calculate duration (end - start) in milliseconds
60
- const durationMs = endTime > 0 && startTime > 0 ? (endTime - startTime) * 1000 : undefined;
61
- // Get or create phase entry
62
- let phase = phaseMap.get(id);
63
- if (!phase) {
64
- phase = {
65
- id,
66
- label: PHASE_LABELS[id] || id,
67
- status: validatedStatus,
68
- durationMs: 0,
69
- };
70
- phaseMap.set(id, phase);
71
- }
72
- // Update status (last status wins)
73
- phase.status = validatedStatus;
74
- // Aggregate duration
75
- if (durationMs !== undefined) {
76
- phase.durationMs = (phase.durationMs || 0) + durationMs;
77
- }
78
- }
79
- return Array.from(phaseMap.values());
80
- }
81
- catch (err) {
82
- logger.warn(`Failed to parse phases file: ${err instanceof Error ? err.message : String(err)}`);
83
- return [];
84
- }
85
- }
9
+ import { parsePhases } from './loop-status.js';
86
10
  /**
87
11
  * Read baseline commit hash from the baseline file.
88
12
  *
@@ -127,7 +51,7 @@ function readBaselineCommit(baselineFilePath) {
127
51
  * which blocks the event loop. Callers should wrap in try-catch to handle
128
52
  * failures gracefully.
129
53
  */
130
- export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
54
+ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature, baselineOverride) {
131
55
  const phasesFilePath = `/tmp/ralph-loop-${feature}.phases`;
132
56
  const baselineFilePath = `/tmp/ralph-loop-${feature}.baseline`;
133
57
  // Parse phases and set implementation iterations from actual loop count
@@ -153,15 +77,20 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
153
77
  completed: basicSummary.tasksDone,
154
78
  total: basicSummary.tasksTotal,
155
79
  };
156
- // Git changes and commits
157
- const baselineCommit = readBaselineCommit(baselineFilePath);
80
+ // Git changes and commits — use override if provided (avoids re-reading a cleaned-up file)
81
+ const baselineCommit = baselineOverride !== undefined
82
+ ? (baselineOverride ? baselineOverride.substring(0, 7) : null)
83
+ : readBaselineCommit(baselineFilePath);
158
84
  const currentCommit = getCurrentCommitHash(projectRoot);
159
85
  let changes = { available: false };
160
86
  let commits = { available: false };
161
87
  if (baselineCommit && currentCommit) {
88
+ // Get commit log between baseline and current
89
+ const commitLog = getCommitList(projectRoot, baselineCommit, currentCommit);
162
90
  commits = {
163
91
  fromHash: baselineCommit,
164
92
  toHash: currentCommit,
93
+ commitList: commitLog?.map((c) => ({ hash: c.hash, title: c.title })),
165
94
  mergeType: 'none', // TODO: Detect merge type from git history
166
95
  available: true,
167
96
  };
@@ -202,13 +131,19 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
202
131
  available: true,
203
132
  created: true,
204
133
  };
205
- // Try to get linked issue, passing prInfo to avoid redundant gh call
206
- const issueInfo = getLinkedIssue(projectRoot, basicSummary.branch, prInfo);
134
+ // Try to get linked issue, passing prInfo and feature name for fallback detection
135
+ const issueInfo = getLinkedIssue(projectRoot, basicSummary.branch, prInfo, feature);
207
136
  if (issueInfo) {
137
+ // When PR is merged with "Closes #N", GitHub auto-closes the issue.
138
+ // The summary may be built before GitHub processes the webhook, so
139
+ // infer closure from PR state to avoid showing stale "OPEN" status.
140
+ const inferredState = prInfo.state === 'MERGED' && issueInfo.state === 'OPEN'
141
+ ? 'CLOSED'
142
+ : issueInfo.state;
208
143
  issue = {
209
144
  number: issueInfo.number,
210
145
  url: issueInfo.url,
211
- status: issueInfo.state,
146
+ status: inferredState,
212
147
  available: true,
213
148
  linked: true,
214
149
  };
@@ -216,6 +151,30 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
216
151
  else {
217
152
  issue = { available: true, linked: false };
218
153
  }
154
+ // Enrich commits from PR when squash-merge detected (1 local commit + merged PR)
155
+ if (prInfo.state === 'MERGED' &&
156
+ commits.available &&
157
+ commits.commitList &&
158
+ commits.commitList.length <= 1) {
159
+ try {
160
+ const prCommitsOutput = execFileSync('gh', ['pr', 'view', String(prInfo.number), '--json', 'commits'], { cwd: projectRoot, encoding: 'utf-8', timeout: 10_000 }).trim();
161
+ const prCommitsData = JSON.parse(prCommitsOutput);
162
+ const prCommits = prCommitsData.commits;
163
+ if (Array.isArray(prCommits) && prCommits.length > 1) {
164
+ commits = {
165
+ ...commits,
166
+ commitList: prCommits.map((c) => ({
167
+ hash: c.oid?.substring(0, 7) ?? '',
168
+ title: c.messageHeadline ?? '',
169
+ })),
170
+ mergeType: 'squash',
171
+ };
172
+ }
173
+ }
174
+ catch (err) {
175
+ logger.debug(`Failed to fetch PR commits: ${err instanceof Error ? err.message : String(err)}`);
176
+ }
177
+ }
219
178
  }
220
179
  else {
221
180
  pr = { available: true, created: false };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Clear the terminal screen to prevent stale PTY scroll buffer artifacts.
3
+ *
4
+ * When Ink re-renders significantly shorter output (e.g. after dismissing a
5
+ * 25-row IssuePicker), the old content remains in the PTY scroll buffer,
6
+ * causing visual duplication of the banner and other content. This sends
7
+ * ANSI escape sequences to clear the entire screen and reset the cursor.
8
+ *
9
+ * This is an intentional mix of imperative stdout writes with Ink's
10
+ * declarative rendering — Ink has no API to flush the PTY scroll buffer.
11
+ * Call this *before* state updates that shrink the rendered output so
12
+ * Ink's next render paints onto a clean screen.
13
+ */
14
+ export declare function clearScreen(stdout: NodeJS.WriteStream): void;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Clear the terminal screen to prevent stale PTY scroll buffer artifacts.
3
+ *
4
+ * When Ink re-renders significantly shorter output (e.g. after dismissing a
5
+ * 25-row IssuePicker), the old content remains in the PTY scroll buffer,
6
+ * causing visual duplication of the banner and other content. This sends
7
+ * ANSI escape sequences to clear the entire screen and reset the cursor.
8
+ *
9
+ * This is an intentional mix of imperative stdout writes with Ink's
10
+ * declarative rendering — Ink has no API to flush the PTY scroll buffer.
11
+ * Call this *before* state updates that shrink the rendered output so
12
+ * Ink's next render paints onto a clean screen.
13
+ */
14
+ export function clearScreen(stdout) {
15
+ stdout.write('\x1b[2J\x1b[H');
16
+ }
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Git utilities for enhanced run summary.
3
3
  */
4
+ export interface CommitLogEntry {
5
+ hash: string;
6
+ title: string;
7
+ }
4
8
  export interface FileDiffStat {
5
9
  path: string;
6
10
  added: number;
@@ -22,3 +26,12 @@ export declare function getCurrentCommitHash(projectRoot: string): string | null
22
26
  * @returns Array of file diff stats, or null if not available
23
27
  */
24
28
  export declare function getDiffStats(projectRoot: string, fromHash: string, toHash: string): FileDiffStat[] | null;
29
+ /**
30
+ * Get the list of commits between two refs.
31
+ *
32
+ * @param projectRoot - Root directory of the git repository
33
+ * @param fromHash - Starting commit hash (exclusive)
34
+ * @param toHash - Ending commit hash (inclusive)
35
+ * @returns Array of commit entries, or null if not available
36
+ */
37
+ export declare function getCommitList(projectRoot: string, fromHash: string, toHash: string): CommitLogEntry[] | null;
@@ -61,3 +61,33 @@ export function getDiffStats(projectRoot, fromHash, toHash) {
61
61
  return null;
62
62
  }
63
63
  }
64
+ /**
65
+ * Get the list of commits between two refs.
66
+ *
67
+ * @param projectRoot - Root directory of the git repository
68
+ * @param fromHash - Starting commit hash (exclusive)
69
+ * @param toHash - Ending commit hash (inclusive)
70
+ * @returns Array of commit entries, or null if not available
71
+ */
72
+ export function getCommitList(projectRoot, fromHash, toHash) {
73
+ try {
74
+ const output = execFileSync('git', ['log', '--oneline', `${fromHash}..${toHash}`], {
75
+ cwd: projectRoot,
76
+ encoding: 'utf-8',
77
+ timeout: 10_000,
78
+ }).trim();
79
+ if (!output) {
80
+ return [];
81
+ }
82
+ return output.split('\n').map((line) => {
83
+ const spaceIdx = line.indexOf(' ');
84
+ if (spaceIdx === -1)
85
+ return { hash: line, title: '' };
86
+ return { hash: line.substring(0, spaceIdx), title: line.substring(spaceIdx + 1) };
87
+ });
88
+ }
89
+ catch (err) {
90
+ logger.warn(`getCommitList failed: ${err instanceof Error ? err.message : String(err)}`);
91
+ return null;
92
+ }
93
+ }
@@ -1,6 +1,13 @@
1
1
  /**
2
2
  * Loop status helpers for the TUI run/monitor screens.
3
3
  */
4
+ /**
5
+ * Find the implementation plan file, checking main project and git worktrees.
6
+ *
7
+ * Shared between loop-status.ts and monitor.ts to avoid duplicating the
8
+ * worktree search logic.
9
+ */
10
+ export declare function findImplementationPlan(projectRoot: string, specsRelPath: string, feature: string): string | null;
4
11
  export interface LoopStatus {
5
12
  running: boolean;
6
13
  phase: string;
@@ -8,13 +15,38 @@ export interface LoopStatus {
8
15
  maxIterations: number;
9
16
  tokensInput: number;
10
17
  tokensOutput: number;
18
+ cacheCreate: number;
19
+ cacheRead: number;
20
+ tokensUpdatedAt?: number;
11
21
  }
12
22
  export interface TaskCounts {
13
23
  tasksDone: number;
14
24
  tasksPending: number;
15
25
  e2eDone: number;
16
26
  e2ePending: number;
27
+ planExists: boolean;
28
+ }
29
+ /**
30
+ * Phase execution status and timing.
31
+ * Shared between RunScreen and loop-status utilities.
32
+ */
33
+ export interface PhaseInfo {
34
+ /** Unique phase identifier (e.g., 'planning', 'implementation') */
35
+ id: string;
36
+ /** Human-readable phase label */
37
+ label: string;
38
+ /** Phase completion status */
39
+ status: 'success' | 'skipped' | 'failed' | 'started';
40
+ /** Duration in milliseconds, if available */
41
+ durationMs?: number;
42
+ /** Number of iterations in this phase (e.g., for implementation) */
43
+ iterations?: number;
17
44
  }
45
+ /**
46
+ * Phase ID to human-readable label mapping.
47
+ * Matches the phase IDs written by feature-loop.sh.
48
+ */
49
+ export declare const PHASE_LABELS: Record<string, string>;
18
50
  /**
19
51
  * Return the conventional log file path for a feature loop.
20
52
  */
@@ -27,6 +59,14 @@ export declare function getLoopLogPath(feature: string): string;
27
59
  * concurrent loops are rare, but callers should be aware of the limitation.
28
60
  */
29
61
  export declare function detectPhase(feature: string): string;
62
+ /**
63
+ * Read the current phase from the `.phases` file written by feature-loop.sh.
64
+ * Returns the human-readable label of the active phase, or null if unavailable.
65
+ *
66
+ * The file format is: phase_id|status|start_timestamp|end_timestamp
67
+ * A line with status "started" and no end timestamp indicates the active phase.
68
+ */
69
+ export declare function readCurrentPhase(feature: string): string | null;
30
70
  /**
31
71
  * Read loop status from temp files written by feature-loop.sh.
32
72
  *
@@ -54,3 +94,57 @@ export declare function getGitBranch(projectRoot: string): string;
54
94
  * Format number with K/M suffix.
55
95
  */
56
96
  export declare function formatNumber(num: number): string;
97
+ /**
98
+ * Format epoch milliseconds as a relative time string (e.g., "30s ago", "2m ago", "1h ago").
99
+ */
100
+ export declare function formatRelativeTime(timestampMs: number): string;
101
+ /**
102
+ * A structured activity event derived from loop log or phase changes.
103
+ */
104
+ export interface ActivityEvent {
105
+ /** Epoch milliseconds when the event occurred */
106
+ timestamp: number;
107
+ /** Human-readable description of the event */
108
+ message: string;
109
+ /** Inferred status based on event content */
110
+ status: 'success' | 'error' | 'in-progress';
111
+ }
112
+ /**
113
+ * Returns true if a log line should be excluded from the activity feed.
114
+ */
115
+ export declare function shouldSkipLine(line: string): boolean;
116
+ /**
117
+ * Parse the loop log file into structured activity events.
118
+ *
119
+ * Filters out noise lines (separators, markdown headers, interactive prompts,
120
+ * config lines) and strips markdown formatting from remaining messages.
121
+ *
122
+ * @param logPath - Absolute path to the loop log file.
123
+ * @param since - Optional epoch ms cutoff; only return events at or after this time.
124
+ */
125
+ export declare function parseLoopLog(logPath: string, since?: number): ActivityEvent[];
126
+ /**
127
+ * Detect phase changes by comparing current phases file to a known previous state,
128
+ * and emit activity events for newly completed or started phases.
129
+ *
130
+ * @param feature - Feature name (used to locate the phases file).
131
+ * @param lastKnownPhases - Phase array from the previous poll cycle.
132
+ */
133
+ export declare function parsePhaseChanges(feature: string, lastKnownPhases?: PhaseInfo[]): {
134
+ events: ActivityEvent[];
135
+ currentPhases?: PhaseInfo[];
136
+ };
137
+ /**
138
+ * Parse phase information from the phases file written by feature-loop.sh.
139
+ *
140
+ * Format: phase_id|status|start_timestamp|end_timestamp
141
+ * Accepts both 3-field lines (started: phase_id|started|timestamp) and
142
+ * 4-field lines (completed: phase_id|status|start_ts|end_ts).
143
+ *
144
+ * The parser handles duplicate phase entries defensively (last status wins,
145
+ * durations aggregate), though feature-loop.sh normally writes one final
146
+ * line per phase.
147
+ *
148
+ * Used by build-run-summary.ts for completion summaries.
149
+ */
150
+ export declare function parsePhases(phasesFilePath: string): PhaseInfo[];