wiggum-cli 0.16.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/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 +2 -0
- package/dist/generator/templates.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -4
- 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 +13 -7
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
- package/dist/tui/app.d.ts +19 -2
- package/dist/tui/app.js +22 -4
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +64 -0
- package/dist/tui/components/RunCompletionSummary.js +6 -3
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
- 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.js +38 -6
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +40 -84
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/loop-status.d.ts +41 -1
- package/dist/tui/utils/loop-status.js +243 -35
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- 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/package.json +4 -1
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AgentScreen - TUI dashboard for the autonomous agent mode
|
|
4
|
+
*
|
|
5
|
+
* Displays issue processing status and an agent log.
|
|
6
|
+
* Two-column layout on wide terminals (>=65 cols), single-column on narrow.
|
|
7
|
+
*
|
|
8
|
+
* Wired to the orchestrator via useAgentOrchestrator hook, which
|
|
9
|
+
* interprets tool calls into structured React state. Console is patched
|
|
10
|
+
* on mount to prevent Ink rendering corruption.
|
|
11
|
+
*
|
|
12
|
+
* Wrapped in AppShell for consistent layout with header and footer.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useCallback, useState, useRef } from 'react';
|
|
15
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
16
|
+
import { AppShell } from '../components/AppShell.js';
|
|
17
|
+
import { colors, phase } from '../theme.js';
|
|
18
|
+
import { useAgentOrchestrator } from '../hooks/useAgentOrchestrator.js';
|
|
19
|
+
const NARROW_BREAKPOINT = 65;
|
|
20
|
+
const SECTION_CHAR = '\u2500'; // ─
|
|
21
|
+
function phaseLabel(p, activeIssue) {
|
|
22
|
+
switch (p) {
|
|
23
|
+
case 'idle': return 'Idle';
|
|
24
|
+
case 'planning': return 'Planning...';
|
|
25
|
+
case 'generating_spec': return 'Generating spec...';
|
|
26
|
+
case 'running_loop': return activeIssue?.loopPhase ?? 'Running loop...';
|
|
27
|
+
case 'reporting': return 'Reporting...';
|
|
28
|
+
case 'reflecting': return 'Reflecting...';
|
|
29
|
+
default: return String(p);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function logLevelColor(level) {
|
|
33
|
+
switch (level) {
|
|
34
|
+
case 'info': return colors.blue;
|
|
35
|
+
case 'warn': return colors.orange;
|
|
36
|
+
case 'error': return colors.pink;
|
|
37
|
+
case 'success': return colors.green;
|
|
38
|
+
default: return colors.gray;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a working status string from active issue state.
|
|
43
|
+
* Issue number/title is already shown in Active Issue panel — just show phase.
|
|
44
|
+
*/
|
|
45
|
+
function workingLabel(activeIssue) {
|
|
46
|
+
if (!activeIssue)
|
|
47
|
+
return 'Starting...';
|
|
48
|
+
return phaseLabel(activeIssue.phase, activeIssue);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the footer phase string from current state.
|
|
52
|
+
* Shows loop sub-phase and iteration instead of duplicating issue title.
|
|
53
|
+
*/
|
|
54
|
+
function footerPhase(status, activeIssue, completedCount, cancelling) {
|
|
55
|
+
if (cancelling)
|
|
56
|
+
return 'Cancelling...';
|
|
57
|
+
if (status === 'idle')
|
|
58
|
+
return 'Waiting';
|
|
59
|
+
if (status === 'complete')
|
|
60
|
+
return `Done — ${completedCount} issue${completedCount === 1 ? '' : 's'} processed`;
|
|
61
|
+
if (status === 'error')
|
|
62
|
+
return 'Error';
|
|
63
|
+
if (!activeIssue)
|
|
64
|
+
return 'Starting...';
|
|
65
|
+
const loopDetail = activeIssue.loopPhase ? ` · ${activeIssue.loopPhase}` : '';
|
|
66
|
+
const iter = activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : '';
|
|
67
|
+
return `#${activeIssue.issueNumber}${loopDetail}${iter}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Horizontal section separator line
|
|
71
|
+
*/
|
|
72
|
+
function SectionSeparator({ width }) {
|
|
73
|
+
const lineWidth = Math.max(1, width - 2); // account for panel padding
|
|
74
|
+
return _jsx(Text, { color: colors.separator, children: SECTION_CHAR.repeat(lineWidth) });
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Issues panel content — shared between wide and narrow layouts
|
|
78
|
+
*/
|
|
79
|
+
function IssuesPanel({ activeIssue, queue, completed, panelWidth, }) {
|
|
80
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Active Issue" }), activeIssue ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: colors.blue, children: ["#", activeIssue.issueNumber] }), _jsxs(Text, { children: [" ", activeIssue.title] })] }), _jsxs(Text, { dimColor: true, children: [phase.active, " ", phaseLabel(activeIssue.phase, activeIssue), activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : ''] })] })) : (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "No active issue" }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Queue", _jsxs(Text, { dimColor: true, children: [" (", queue.length, ")"] })] }), queue.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "Empty" }) })) : (queue.slice(0, 5).map((issue) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["#", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }) }, issue.issueNumber)))), queue.length > 5 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["...and ", queue.length - 5, " more"] }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Completed", _jsxs(Text, { dimColor: true, children: [" (", completed.length, ")"] })] }), completed.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "None yet" }) })) : (completed.slice(-5).map((issue) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: issue.error ? colors.pink : colors.green, children: issue.error ? phase.error : phase.complete }), _jsxs(Text, { dimColor: true, children: [" #", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), issue.prUrl && (_jsxs(Text, { dimColor: true, children: [" \u2514 PR: ", issue.prUrl] }))] }, issue.issueNumber))))] }));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Log panel content — shared between wide and narrow layouts
|
|
84
|
+
*/
|
|
85
|
+
const LOG_TAIL_SIZE = 20;
|
|
86
|
+
function LogPanel({ logEntries }) {
|
|
87
|
+
const visible = logEntries.slice(-LOG_TAIL_SIZE);
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Agent Log" }), visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for agent activity..." })) : (visible.map((entry, index) => (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: entry.timestamp.slice(11, 19) }), _jsx(Text, { children: " " }), _jsx(Text, { color: logLevelColor(entry.level), children: entry.message })] }) }, `${entry.timestamp}-${index}`))))] }));
|
|
89
|
+
}
|
|
90
|
+
export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
|
|
91
|
+
const { stdout } = useStdout();
|
|
92
|
+
const columns = stdout?.columns ?? 80;
|
|
93
|
+
const isNarrow = columns < NARROW_BREAKPOINT;
|
|
94
|
+
// Patch console on mount to prevent Ink rendering corruption
|
|
95
|
+
// from any console.* calls in the orchestrator or AI SDK.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
let restore;
|
|
98
|
+
import('patch-console').then((mod) => {
|
|
99
|
+
const patchConsole = mod.default;
|
|
100
|
+
restore = patchConsole(() => {
|
|
101
|
+
// Swallow console output — Ink renders its own UI
|
|
102
|
+
});
|
|
103
|
+
}).catch(() => {
|
|
104
|
+
// patch-console not available — continue without it
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
restore?.();
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
const { status, activeIssue, queue, completed, logEntries, error, abort } = useAgentOrchestrator({
|
|
111
|
+
projectRoot,
|
|
112
|
+
modelOverride: agentOptions?.modelOverride,
|
|
113
|
+
maxItems: agentOptions?.maxItems,
|
|
114
|
+
maxSteps: agentOptions?.maxSteps,
|
|
115
|
+
labels: agentOptions?.labels,
|
|
116
|
+
reviewMode: agentOptions?.reviewMode,
|
|
117
|
+
dryRun: agentOptions?.dryRun,
|
|
118
|
+
});
|
|
119
|
+
// Track whether the user requested cancellation
|
|
120
|
+
const [cancelling, setCancelling] = useState(false);
|
|
121
|
+
const onExitRef = useRef(onExit);
|
|
122
|
+
onExitRef.current = onExit;
|
|
123
|
+
// When cancelling and the orchestrator finishes, exit
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (cancelling && (status === 'complete' || status === 'error')) {
|
|
126
|
+
onExitRef.current?.();
|
|
127
|
+
}
|
|
128
|
+
}, [cancelling, status]);
|
|
129
|
+
const isWorking = status === 'running' || cancelling;
|
|
130
|
+
useInput(useCallback((input, key) => {
|
|
131
|
+
if (input === 'q' || key.escape) {
|
|
132
|
+
if (status === 'running') {
|
|
133
|
+
// Signal abort — stay mounted so cleanup can propagate to subprocesses
|
|
134
|
+
setCancelling(true);
|
|
135
|
+
abort();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Already done — exit immediately
|
|
139
|
+
onExitRef.current?.();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}, [abort, status]));
|
|
143
|
+
const tips = cancelling ? 'Cancelling...' : 'q exit │ Esc back';
|
|
144
|
+
if (isNarrow) {
|
|
145
|
+
const panelWidth = Math.max(20, columns - 4);
|
|
146
|
+
return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: {
|
|
147
|
+
action: 'Agent',
|
|
148
|
+
phase: footerPhase(status, activeIssue, completed.length, cancelling),
|
|
149
|
+
}, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: panelWidth }) }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries }) })] }) }));
|
|
150
|
+
}
|
|
151
|
+
// Wide layout: two-column
|
|
152
|
+
const leftWidth = Math.max(30, Math.floor(columns * 0.4));
|
|
153
|
+
const rightWidth = Math.max(30, columns - leftWidth - 3);
|
|
154
|
+
const leftInnerWidth = leftWidth - 4; // border (2) + paddingX (2)
|
|
155
|
+
return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: {
|
|
156
|
+
action: 'Agent',
|
|
157
|
+
phase: footerPhase(status, activeIssue, completed.length, cancelling),
|
|
158
|
+
}, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Box, { flexDirection: "column", width: leftWidth, borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: leftInnerWidth }) }), _jsx(Box, { flexDirection: "column", width: rightWidth, borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries }) })] }) }));
|
|
159
|
+
}
|
|
@@ -186,6 +186,10 @@ export function InitScreen({ header, projectRoot, sessionState, onComplete, onCa
|
|
|
186
186
|
existingFiles: 'backup',
|
|
187
187
|
generateConfig: true,
|
|
188
188
|
verbose: false,
|
|
189
|
+
customVariables: {
|
|
190
|
+
...(state.provider ? { agentProvider: state.provider } : {}),
|
|
191
|
+
...(state.model ? { agentModel: state.model } : {}),
|
|
192
|
+
},
|
|
189
193
|
});
|
|
190
194
|
try {
|
|
191
195
|
setGenerating('Writing configuration files...');
|
|
@@ -34,6 +34,8 @@ export interface InterviewScreenProps {
|
|
|
34
34
|
scanResult?: ScanResult;
|
|
35
35
|
/** Path to specs directory (relative to project root, defaults to '.ralph/specs') */
|
|
36
36
|
specsPath?: string;
|
|
37
|
+
/** References to auto-add during context phase (from CLI --issue/--context flags) */
|
|
38
|
+
initialReferences?: string[];
|
|
37
39
|
/** Called when spec generation is complete - receives spec, messages, and specPath */
|
|
38
40
|
onComplete: (spec: string, messages: Message[], specPath: string) => void;
|
|
39
41
|
/** Called when user cancels the interview */
|
|
@@ -45,4 +47,4 @@ export interface InterviewScreenProps {
|
|
|
45
47
|
* The main screen for the /new command interview flow. Combines all TUI
|
|
46
48
|
* components within an AppShell layout.
|
|
47
49
|
*/
|
|
48
|
-
export declare function InterviewScreen({ header, featureName, projectRoot, provider, model, scanResult, specsPath, onComplete, onCancel, }: InterviewScreenProps): React.ReactElement;
|
|
50
|
+
export declare function InterviewScreen({ header, featureName, projectRoot, provider, model, scanResult, specsPath, initialReferences, onComplete, onCancel, }: InterviewScreenProps): React.ReactElement;
|
|
@@ -14,11 +14,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
14
14
|
* shows SpecCompletionSummary inline before returning to shell.
|
|
15
15
|
*/
|
|
16
16
|
import { useEffect, useCallback, useRef, useState } from 'react';
|
|
17
|
-
import { Box, Text, useInput } from 'ink';
|
|
17
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
18
18
|
import { logger } from '../../utils/logger.js';
|
|
19
19
|
import { MessageList } from '../components/MessageList.js';
|
|
20
20
|
import { ChatInput } from '../components/ChatInput.js';
|
|
21
21
|
import { MultiSelect } from '../components/MultiSelect.js';
|
|
22
|
+
import { IssuePicker } from '../components/IssuePicker.js';
|
|
22
23
|
import { AppShell } from '../components/AppShell.js';
|
|
23
24
|
import { SpecCompletionSummary } from '../components/SpecCompletionSummary.js';
|
|
24
25
|
import { useSpecGenerator, PHASE_CONFIGS, TOTAL_DISPLAY_PHASES, } from '../hooks/useSpecGenerator.js';
|
|
@@ -28,13 +29,15 @@ import { loadContext, toScanResultFromPersisted, getContextAge, } from '../../co
|
|
|
28
29
|
import { join } from 'node:path';
|
|
29
30
|
import { initTracing, flushTracing } from '../../utils/tracing.js';
|
|
30
31
|
import { resolveOptionLabels } from '../types/interview.js';
|
|
32
|
+
import { isGhInstalled, detectGitHubRemote, listRepoIssues, fetchGitHubIssue, } from '../../utils/github.js';
|
|
33
|
+
import { clearScreen } from '../utils/clear-screen.js';
|
|
31
34
|
/**
|
|
32
35
|
* InterviewScreen component
|
|
33
36
|
*
|
|
34
37
|
* The main screen for the /new command interview flow. Combines all TUI
|
|
35
38
|
* components within an AppShell layout.
|
|
36
39
|
*/
|
|
37
|
-
export function InterviewScreen({ header, featureName, projectRoot, provider, model, scanResult, specsPath = '.ralph/specs', onComplete, onCancel, }) {
|
|
40
|
+
export function InterviewScreen({ header, featureName, projectRoot, provider, model, scanResult, specsPath = '.ralph/specs', initialReferences, onComplete, onCancel, }) {
|
|
38
41
|
const { state, initialize, addMessage, addStreamingMessage, updateStreamingMessage, completeStreamingMessage, startToolCall, completeToolCall, setPhase, setGeneratedSpec, setError, setWorking, setReady, } = useSpecGenerator();
|
|
39
42
|
const orchestratorRef = useRef(null);
|
|
40
43
|
const isStreamingRef = useRef(false);
|
|
@@ -47,6 +50,13 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
47
50
|
messagesRef.current = state.messages;
|
|
48
51
|
const [toolCallsExpanded, setToolCallsExpanded] = useState(false);
|
|
49
52
|
const [currentQuestion, setCurrentQuestion] = useState(null);
|
|
53
|
+
const { stdout } = useStdout();
|
|
54
|
+
const [issuePickerVisible, setIssuePickerVisible] = useState(false);
|
|
55
|
+
const [issuePickerIssues, setIssuePickerIssues] = useState([]);
|
|
56
|
+
const [issuePickerLoading, setIssuePickerLoading] = useState(false);
|
|
57
|
+
const [issuePickerError, setIssuePickerError] = useState();
|
|
58
|
+
const [issuePickerRepo, setIssuePickerRepo] = useState(null);
|
|
59
|
+
const [hasReferences, setHasReferences] = useState(false);
|
|
50
60
|
// Completion state: when spec is done, show summary inline
|
|
51
61
|
const [completionData, setCompletionData] = useState(null);
|
|
52
62
|
useEffect(() => {
|
|
@@ -202,6 +212,117 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
202
212
|
orchestratorRef.current = null;
|
|
203
213
|
};
|
|
204
214
|
}, [featureName, projectRoot, provider, model, scanResult, specsPath]);
|
|
215
|
+
// Process initial references from CLI --issue/--context flags
|
|
216
|
+
const initialRefsProcessed = useRef(false);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const orchestrator = orchestratorRef.current;
|
|
219
|
+
if (!orchestrator || !initialReferences?.length || initialRefsProcessed.current)
|
|
220
|
+
return;
|
|
221
|
+
if (state.phase !== 'context')
|
|
222
|
+
return;
|
|
223
|
+
if (!state.awaitingInput)
|
|
224
|
+
return;
|
|
225
|
+
initialRefsProcessed.current = true;
|
|
226
|
+
(async () => {
|
|
227
|
+
for (const ref of initialReferences) {
|
|
228
|
+
if (ref.startsWith('issue:')) {
|
|
229
|
+
const value = ref.slice(6);
|
|
230
|
+
if (/^\d+$/.test(value)) {
|
|
231
|
+
// Bare issue number — resolve from repo remote
|
|
232
|
+
const repo = await detectGitHubRemote(projectRoot);
|
|
233
|
+
if (repo) {
|
|
234
|
+
const detail = await fetchGitHubIssue(repo.owner, repo.repo, parseInt(value, 10));
|
|
235
|
+
if (detail) {
|
|
236
|
+
const content = `# ${detail.title}\n\n${detail.body ?? ''}`;
|
|
237
|
+
orchestrator.addReferenceContent(content, `GitHub issue #${value}`);
|
|
238
|
+
addMessage('system', `Added: GitHub issue #${value} ${detail.title}`);
|
|
239
|
+
setHasReferences(true);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
addMessage('system', `Could not fetch issue #${value} — no GitHub remote detected or gh CLI unavailable`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Full URL — use existing addReference which handles GitHub URLs
|
|
247
|
+
addMessage('user', value);
|
|
248
|
+
const added = await orchestrator.addReference(value);
|
|
249
|
+
if (added)
|
|
250
|
+
setHasReferences(true);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
addMessage('user', ref);
|
|
255
|
+
const added = await orchestrator.addReference(ref);
|
|
256
|
+
if (added)
|
|
257
|
+
setHasReferences(true);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
})();
|
|
261
|
+
}, [state.phase, state.awaitingInput, initialReferences, projectRoot, addMessage]);
|
|
262
|
+
const handleIssueCommand = useCallback(async (searchQuery) => {
|
|
263
|
+
setIssuePickerVisible(true);
|
|
264
|
+
setIssuePickerLoading(true);
|
|
265
|
+
setIssuePickerError(undefined);
|
|
266
|
+
try {
|
|
267
|
+
const ghAvailable = await isGhInstalled();
|
|
268
|
+
if (!ghAvailable) {
|
|
269
|
+
setIssuePickerError('Install GitHub CLI (gh) for issue browsing');
|
|
270
|
+
setIssuePickerLoading(false);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
let repo = issuePickerRepo;
|
|
274
|
+
if (!repo) {
|
|
275
|
+
repo = await detectGitHubRemote(projectRoot);
|
|
276
|
+
if (!repo) {
|
|
277
|
+
setIssuePickerError('No GitHub remote detected in this project');
|
|
278
|
+
setIssuePickerLoading(false);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
setIssuePickerRepo(repo);
|
|
282
|
+
}
|
|
283
|
+
const result = await listRepoIssues(repo.owner, repo.repo, searchQuery);
|
|
284
|
+
if (result.error) {
|
|
285
|
+
setIssuePickerError(result.error);
|
|
286
|
+
}
|
|
287
|
+
setIssuePickerIssues(result.issues);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
setIssuePickerError(err instanceof Error ? err.message : String(err));
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
setIssuePickerLoading(false);
|
|
294
|
+
}
|
|
295
|
+
}, [projectRoot, issuePickerRepo]);
|
|
296
|
+
const handleIssueSelect = useCallback(async (issue) => {
|
|
297
|
+
clearScreen(stdout);
|
|
298
|
+
setIssuePickerVisible(false);
|
|
299
|
+
setIssuePickerIssues([]);
|
|
300
|
+
const orchestrator = orchestratorRef.current;
|
|
301
|
+
if (!orchestrator)
|
|
302
|
+
return;
|
|
303
|
+
const repo = issuePickerRepo;
|
|
304
|
+
if (!repo)
|
|
305
|
+
return;
|
|
306
|
+
setWorking(true, `Fetching issue #${issue.number}...`);
|
|
307
|
+
const detail = await fetchGitHubIssue(repo.owner, repo.repo, issue.number);
|
|
308
|
+
if (detail) {
|
|
309
|
+
const content = `# ${detail.title}\n\n${detail.body ?? ''}`;
|
|
310
|
+
orchestrator.addReferenceContent(content, `GitHub issue #${issue.number}`);
|
|
311
|
+
const labelStr = detail.labels.length > 0 ? ` [${detail.labels.join(', ')}]` : '';
|
|
312
|
+
addMessage('system', `\u2713 #${issue.number} ${detail.title}${labelStr}`);
|
|
313
|
+
setHasReferences(true);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
addMessage('system', `Failed to fetch issue #${issue.number}`);
|
|
317
|
+
}
|
|
318
|
+
setReady();
|
|
319
|
+
}, [stdout, issuePickerRepo, addMessage, setWorking, setReady]);
|
|
320
|
+
const handleIssueCancel = useCallback(() => {
|
|
321
|
+
clearScreen(stdout);
|
|
322
|
+
setIssuePickerVisible(false);
|
|
323
|
+
setIssuePickerIssues([]);
|
|
324
|
+
setIssuePickerError(undefined);
|
|
325
|
+
}, [stdout]);
|
|
205
326
|
const handleSubmit = useCallback(async (value) => {
|
|
206
327
|
try {
|
|
207
328
|
const orchestrator = orchestratorRef.current;
|
|
@@ -209,19 +330,30 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
209
330
|
logger.debug('Interview submit ignored: orchestrator not ready yet');
|
|
210
331
|
return;
|
|
211
332
|
}
|
|
212
|
-
|
|
333
|
+
const currentPhase = orchestrator.getPhase();
|
|
334
|
+
// In context phase, /issue is a command — don't echo it as user content
|
|
335
|
+
const isIssueCmd = currentPhase === 'context' && /^\/issue(?:\s|$)/i.test(value);
|
|
336
|
+
if (value && !isIssueCmd) {
|
|
213
337
|
addMessage('user', value);
|
|
214
338
|
}
|
|
215
|
-
const currentPhase = orchestrator.getPhase();
|
|
216
339
|
switch (currentPhase) {
|
|
217
|
-
case 'context':
|
|
340
|
+
case 'context': {
|
|
341
|
+
const issueMatch = value.match(/^\/issue(?:\s+(.+))?$/i);
|
|
342
|
+
if (issueMatch) {
|
|
343
|
+
const searchQuery = issueMatch[1]?.trim();
|
|
344
|
+
await handleIssueCommand(searchQuery);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
218
347
|
if (value) {
|
|
219
|
-
await orchestrator.addReference(value);
|
|
348
|
+
const added = await orchestrator.addReference(value);
|
|
349
|
+
if (added)
|
|
350
|
+
setHasReferences(true);
|
|
220
351
|
}
|
|
221
352
|
else {
|
|
222
353
|
await orchestrator.advanceToGoals();
|
|
223
354
|
}
|
|
224
355
|
break;
|
|
356
|
+
}
|
|
225
357
|
case 'goals':
|
|
226
358
|
await orchestrator.submitGoals(value);
|
|
227
359
|
break;
|
|
@@ -247,7 +379,7 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
247
379
|
logger.error(`Interview submit failed: ${reason}`);
|
|
248
380
|
setError(reason);
|
|
249
381
|
}
|
|
250
|
-
}, [addMessage, currentQuestion, setError]);
|
|
382
|
+
}, [addMessage, currentQuestion, setError, handleIssueCommand]);
|
|
251
383
|
const handleMultiSelectSubmit = useCallback(async (selectedValues) => {
|
|
252
384
|
try {
|
|
253
385
|
const orchestrator = orchestratorRef.current;
|
|
@@ -294,6 +426,8 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
294
426
|
return;
|
|
295
427
|
}
|
|
296
428
|
if (key.escape) {
|
|
429
|
+
if (issuePickerVisible)
|
|
430
|
+
return;
|
|
297
431
|
if (currentQuestion) {
|
|
298
432
|
setCurrentQuestion(null);
|
|
299
433
|
return;
|
|
@@ -312,7 +446,7 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
312
446
|
return null;
|
|
313
447
|
switch (state.phase) {
|
|
314
448
|
case 'context':
|
|
315
|
-
return 'Enter URLs or
|
|
449
|
+
return 'Enter URLs, file paths, or /issue to browse. Enter \u21b5 to continue.';
|
|
316
450
|
case 'goals':
|
|
317
451
|
return 'Describe what you want to build.';
|
|
318
452
|
case 'interview':
|
|
@@ -330,7 +464,9 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
330
464
|
const getPlaceholder = () => {
|
|
331
465
|
switch (state.phase) {
|
|
332
466
|
case 'context':
|
|
333
|
-
return
|
|
467
|
+
return hasReferences
|
|
468
|
+
? 'Add more references, or press Enter \u21b5 to continue to Goals...'
|
|
469
|
+
: 'Enter URL, file path, /issue, or paste text (prefix "text:" for inline)...';
|
|
334
470
|
case 'goals':
|
|
335
471
|
return 'Describe what you want to build...';
|
|
336
472
|
case 'interview':
|
|
@@ -355,7 +491,7 @@ export function InterviewScreen({ header, featureName, projectRoot, provider, mo
|
|
|
355
491
|
})), onSubmit: handleMultiSelectSubmit, onChatMode: handleChatMode }, currentQuestion.id));
|
|
356
492
|
}
|
|
357
493
|
else {
|
|
358
|
-
inputElement = (_jsx(ChatInput, { onSubmit: handleSubmit, disabled: inputDisabled, allowEmpty: state.phase === 'context', placeholder: getPlaceholder() }));
|
|
494
|
+
inputElement = (_jsxs(Box, { flexDirection: "column", children: [_jsx(ChatInput, { onSubmit: handleSubmit, disabled: inputDisabled || issuePickerVisible, allowEmpty: state.phase === 'context', placeholder: getPlaceholder() }), issuePickerVisible && (_jsx(IssuePicker, { issues: issuePickerIssues, repoSlug: issuePickerRepo ? `${issuePickerRepo.owner}/${issuePickerRepo.repo}` : '...', onSelect: handleIssueSelect, onCancel: handleIssueCancel, isLoading: issuePickerLoading, error: issuePickerError }))] }));
|
|
359
495
|
}
|
|
360
496
|
}
|
|
361
497
|
return (_jsx(AppShell, { header: header, tips: getTips(), isWorking: state.isWorking && !completionData, workingStatus: state.workingStatus, workingHint: "esc to cancel", error: state.error, input: inputElement, footerStatus: {
|
|
@@ -11,7 +11,7 @@ import type { BackgroundRun } from '../hooks/useBackgroundRuns.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Navigation targets for the shell
|
|
13
13
|
*/
|
|
14
|
-
export type NavigationTarget = 'shell' | 'interview' | 'init' | 'run';
|
|
14
|
+
export type NavigationTarget = 'shell' | 'interview' | 'init' | 'run' | 'agent';
|
|
15
15
|
/**
|
|
16
16
|
* Navigation props passed to target screens
|
|
17
17
|
*/
|
|
@@ -171,6 +171,38 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
171
171
|
}
|
|
172
172
|
addSystemMessage(`No running loop found for "${featureName}".`);
|
|
173
173
|
}, [addSystemMessage, backgroundRuns, onNavigate]);
|
|
174
|
+
const handleAgent = useCallback((args) => {
|
|
175
|
+
if (!sessionState.initialized) {
|
|
176
|
+
addSystemMessage('Project not initialized. Run /init first.');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Parse optional flags
|
|
180
|
+
let dryRun = false;
|
|
181
|
+
let maxItems;
|
|
182
|
+
let reviewMode;
|
|
183
|
+
for (let i = 0; i < args.length; i++) {
|
|
184
|
+
if (args[i] === '--dry-run') {
|
|
185
|
+
dryRun = true;
|
|
186
|
+
}
|
|
187
|
+
else if (args[i] === '--max-items' && i + 1 < args.length) {
|
|
188
|
+
maxItems = parseInt(args[i + 1], 10);
|
|
189
|
+
if (Number.isNaN(maxItems)) {
|
|
190
|
+
addSystemMessage(`Invalid --max-items value '${args[i + 1]}'. Must be a number.`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
else if (args[i] === '--review-mode' && i + 1 < args.length) {
|
|
196
|
+
reviewMode = args[i + 1];
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (reviewMode !== undefined && !['manual', 'auto', 'merge'].includes(reviewMode)) {
|
|
201
|
+
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual', 'auto', or 'merge'.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
onNavigate('agent', { dryRun, maxItems, reviewMode });
|
|
205
|
+
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
174
206
|
const handleConfig = useCallback((args) => {
|
|
175
207
|
if (args.length === 0) {
|
|
176
208
|
addSystemMessage('Config management - not yet implemented in TUI mode. Use CLI: wiggum config');
|
|
@@ -220,6 +252,9 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
220
252
|
case 'monitor':
|
|
221
253
|
handleMonitor(args);
|
|
222
254
|
break;
|
|
255
|
+
case 'agent':
|
|
256
|
+
handleAgent(args);
|
|
257
|
+
break;
|
|
223
258
|
case 'config':
|
|
224
259
|
handleConfig(args);
|
|
225
260
|
break;
|
|
@@ -229,7 +264,7 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
229
264
|
default:
|
|
230
265
|
addSystemMessage(`Unknown command: ${commandName}`);
|
|
231
266
|
}
|
|
232
|
-
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleConfig, handleExit, addSystemMessage]);
|
|
267
|
+
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleAgent, handleConfig, handleExit, addSystemMessage]);
|
|
233
268
|
const handleNaturalLanguage = useCallback((_text) => {
|
|
234
269
|
addSystemMessage('Tip: Use /help to see available commands, or /new <feature> to create a spec.');
|
|
235
270
|
}, [addSystemMessage]);
|
|
@@ -101,6 +101,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
101
101
|
tasksPending: 0,
|
|
102
102
|
e2eDone: 0,
|
|
103
103
|
e2ePending: 0,
|
|
104
|
+
planExists: false,
|
|
104
105
|
});
|
|
105
106
|
const [branch, setBranch] = useState('-');
|
|
106
107
|
const [error, setError] = useState(null);
|
|
@@ -124,6 +125,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
124
125
|
const handledActionIdRef = useRef(null);
|
|
125
126
|
const lastLogLineCountRef = useRef(0);
|
|
126
127
|
const lastKnownPhasesRef = useRef(undefined);
|
|
128
|
+
const lastActivityTimeRef = useRef(Date.now());
|
|
129
|
+
const lastCommitForEventRef = useRef(null);
|
|
127
130
|
// Read baseline commit once on mount
|
|
128
131
|
useEffect(() => {
|
|
129
132
|
const baselinePath = `/tmp/ralph-loop-${featureName}.baseline`;
|
|
@@ -198,11 +201,33 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
198
201
|
if (currentPhases) {
|
|
199
202
|
lastKnownPhasesRef.current = currentPhases;
|
|
200
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;
|
|
201
213
|
const MAX_STORED_EVENTS = 100;
|
|
202
214
|
const newEvents = [...newLogEvents, ...phaseEvents];
|
|
203
215
|
if (newEvents.length > 0 && isMountedRef.current) {
|
|
216
|
+
lastActivityTimeRef.current = Date.now();
|
|
204
217
|
setActivityEvents((prev) => [...prev, ...newEvents].slice(-MAX_STORED_EVENTS));
|
|
205
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
|
+
}
|
|
206
231
|
// Check for pending action request (loop waiting for user input)
|
|
207
232
|
const request = readActionRequest(featureName);
|
|
208
233
|
if (!isMountedRef.current)
|
|
@@ -230,6 +255,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
230
255
|
const tasksDone = nextTasks.tasksDone + nextTasks.e2eDone;
|
|
231
256
|
const tasksTotal = tasksDone + nextTasks.tasksPending + nextTasks.e2ePending;
|
|
232
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}`;
|
|
233
261
|
const basicSummary = {
|
|
234
262
|
feature: featureName,
|
|
235
263
|
iterations: nextStatus.iteration,
|
|
@@ -242,13 +270,13 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
242
270
|
cacheRead: nextStatus.cacheRead,
|
|
243
271
|
exitCode,
|
|
244
272
|
exitCodeInferred: true,
|
|
245
|
-
branch:
|
|
273
|
+
branch: summaryBranch,
|
|
246
274
|
logPath,
|
|
247
275
|
errorTail,
|
|
248
276
|
};
|
|
249
277
|
let enhancedSummary;
|
|
250
278
|
try {
|
|
251
|
-
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
279
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
|
|
252
280
|
}
|
|
253
281
|
catch (err) {
|
|
254
282
|
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -260,7 +288,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
260
288
|
logger.error(`Failed to persist summary file for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
261
289
|
});
|
|
262
290
|
}
|
|
263
|
-
}, [featureName, projectRoot, monitorOnly]);
|
|
291
|
+
}, [featureName, projectRoot, monitorOnly, baselineCommit]);
|
|
264
292
|
// Keep a stable ref to the latest refreshStatus so the spawn effect
|
|
265
293
|
// can schedule polls without re-running when refreshStatus changes.
|
|
266
294
|
const refreshStatusRef = useRef(refreshStatus);
|
|
@@ -425,6 +453,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
425
453
|
closeSync(logFd);
|
|
426
454
|
logFdClosed = true;
|
|
427
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));
|
|
428
460
|
if (!isMountedRef.current)
|
|
429
461
|
return;
|
|
430
462
|
let latestStatus;
|
|
@@ -436,7 +468,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
436
468
|
catch (err) {
|
|
437
469
|
logger.error(`Failed to read final run status for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
438
470
|
latestStatus = { running: false, iteration: 0, maxIterations: config.loop.maxIterations, phase: 'unknown', tokensInput: 0, tokensOutput: 0, cacheCreate: 0, cacheRead: 0 };
|
|
439
|
-
latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0 };
|
|
471
|
+
latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0, planExists: false };
|
|
440
472
|
}
|
|
441
473
|
const tasksDone = latestTasks.tasksDone + latestTasks.e2eDone;
|
|
442
474
|
const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
|
|
@@ -453,14 +485,14 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
453
485
|
cacheCreate: latestStatus.cacheCreate,
|
|
454
486
|
cacheRead: latestStatus.cacheRead,
|
|
455
487
|
exitCode,
|
|
456
|
-
branch:
|
|
488
|
+
branch: `feat/${featureName}`,
|
|
457
489
|
logPath,
|
|
458
490
|
errorTail,
|
|
459
491
|
};
|
|
460
492
|
// Build enhanced summary with phases, git stats, PR/issue metadata
|
|
461
493
|
let enhancedSummary;
|
|
462
494
|
try {
|
|
463
|
-
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
495
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
|
|
464
496
|
}
|
|
465
497
|
catch (err) {
|
|
466
498
|
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -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;
|