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.
- package/README.md +7 -1
- package/bin/ralph.js +0 -0
- package/dist/agent/memory/ingest.d.ts +14 -0
- package/dist/agent/memory/ingest.js +77 -0
- package/dist/agent/memory/store.d.ts +15 -0
- package/dist/agent/memory/store.js +98 -0
- package/dist/agent/memory/types.d.ts +16 -0
- package/dist/agent/memory/types.js +14 -0
- package/dist/agent/orchestrator.d.ts +7 -0
- package/dist/agent/orchestrator.js +266 -0
- package/dist/agent/resolve-config.d.ts +26 -0
- package/dist/agent/resolve-config.js +43 -0
- package/dist/agent/tools/backlog.d.ts +27 -0
- package/dist/agent/tools/backlog.js +51 -0
- package/dist/agent/tools/dry-run.d.ts +106 -0
- package/dist/agent/tools/dry-run.js +119 -0
- package/dist/agent/tools/execution.d.ts +51 -0
- package/dist/agent/tools/execution.js +256 -0
- package/dist/agent/tools/feature-state.d.ts +43 -0
- package/dist/agent/tools/feature-state.js +184 -0
- package/dist/agent/tools/introspection.d.ts +23 -0
- package/dist/agent/tools/introspection.js +40 -0
- package/dist/agent/tools/memory.d.ts +44 -0
- package/dist/agent/tools/memory.js +99 -0
- package/dist/agent/tools/preflight.d.ts +7 -0
- package/dist/agent/tools/preflight.js +137 -0
- package/dist/agent/tools/reporting.d.ts +58 -0
- package/dist/agent/tools/reporting.js +119 -0
- package/dist/agent/tools/schemas.d.ts +2 -0
- package/dist/agent/tools/schemas.js +3 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/ai/conversation/conversation-manager.js +8 -0
- package/dist/ai/conversation/url-fetcher.js +27 -0
- package/dist/ai/providers.js +5 -5
- package/dist/commands/agent.d.ts +17 -0
- package/dist/commands/agent.js +114 -0
- package/dist/commands/monitor.js +50 -183
- package/dist/commands/new-auto.d.ts +15 -0
- package/dist/commands/new-auto.js +237 -0
- package/dist/commands/run.js +20 -10
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.js +68 -0
- package/dist/generator/config.d.ts +1 -41
- package/dist/generator/config.js +7 -0
- package/dist/generator/index.d.ts +2 -2
- package/dist/generator/templates.d.ts +3 -0
- package/dist/generator/templates.js +22 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +333 -40
- package/dist/repl/command-parser.d.ts +5 -0
- package/dist/repl/command-parser.js +5 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +39 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +611 -95
- package/dist/tui/app.d.ts +34 -2
- package/dist/tui/app.js +31 -5
- package/dist/tui/components/ActivityFeed.d.ts +18 -0
- package/dist/tui/components/ActivityFeed.js +31 -0
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +64 -0
- package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
- package/dist/tui/components/RunCompletionSummary.js +103 -10
- package/dist/tui/components/SummaryBox.d.ts +4 -0
- package/dist/tui/components/SummaryBox.js +4 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
- package/dist/tui/hooks/useBackgroundRuns.js +1 -1
- package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
- package/dist/tui/orchestration/interview-orchestrator.js +27 -6
- package/dist/tui/screens/AgentScreen.d.ts +21 -0
- package/dist/tui/screens/AgentScreen.js +159 -0
- package/dist/tui/screens/InitScreen.js +4 -0
- package/dist/tui/screens/InterviewScreen.d.ts +3 -1
- package/dist/tui/screens/InterviewScreen.js +146 -10
- package/dist/tui/screens/MainShell.d.ts +1 -1
- package/dist/tui/screens/MainShell.js +36 -1
- package/dist/tui/screens/RunScreen.d.ts +15 -15
- package/dist/tui/screens/RunScreen.js +96 -11
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +44 -85
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/git-summary.d.ts +13 -0
- package/dist/tui/utils/git-summary.js +30 -0
- package/dist/tui/utils/loop-status.d.ts +94 -0
- package/dist/tui/utils/loop-status.js +430 -10
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- package/dist/utils/ci.d.ts +8 -0
- package/dist/utils/ci.js +13 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/github.d.ts +32 -0
- package/dist/utils/github.js +106 -0
- package/dist/utils/spec-names.js +5 -1
- package/package.json +10 -2
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
- package/src/templates/prompts/PROMPT_feature.md.tmpl +39 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- 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:
|
|
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:
|
|
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 {
|
|
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 =
|
|
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
|
|
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:
|
|
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[];
|