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.
Files changed (97) hide show
  1. package/bin/ralph.js +0 -0
  2. package/dist/agent/memory/ingest.d.ts +14 -0
  3. package/dist/agent/memory/ingest.js +77 -0
  4. package/dist/agent/memory/store.d.ts +15 -0
  5. package/dist/agent/memory/store.js +98 -0
  6. package/dist/agent/memory/types.d.ts +16 -0
  7. package/dist/agent/memory/types.js +14 -0
  8. package/dist/agent/orchestrator.d.ts +7 -0
  9. package/dist/agent/orchestrator.js +266 -0
  10. package/dist/agent/resolve-config.d.ts +26 -0
  11. package/dist/agent/resolve-config.js +43 -0
  12. package/dist/agent/tools/backlog.d.ts +27 -0
  13. package/dist/agent/tools/backlog.js +51 -0
  14. package/dist/agent/tools/dry-run.d.ts +106 -0
  15. package/dist/agent/tools/dry-run.js +119 -0
  16. package/dist/agent/tools/execution.d.ts +51 -0
  17. package/dist/agent/tools/execution.js +256 -0
  18. package/dist/agent/tools/feature-state.d.ts +43 -0
  19. package/dist/agent/tools/feature-state.js +184 -0
  20. package/dist/agent/tools/introspection.d.ts +23 -0
  21. package/dist/agent/tools/introspection.js +40 -0
  22. package/dist/agent/tools/memory.d.ts +44 -0
  23. package/dist/agent/tools/memory.js +99 -0
  24. package/dist/agent/tools/preflight.d.ts +7 -0
  25. package/dist/agent/tools/preflight.js +137 -0
  26. package/dist/agent/tools/reporting.d.ts +58 -0
  27. package/dist/agent/tools/reporting.js +119 -0
  28. package/dist/agent/tools/schemas.d.ts +2 -0
  29. package/dist/agent/tools/schemas.js +3 -0
  30. package/dist/agent/types.d.ts +45 -0
  31. package/dist/agent/types.js +1 -0
  32. package/dist/ai/conversation/conversation-manager.js +8 -0
  33. package/dist/ai/conversation/url-fetcher.js +27 -0
  34. package/dist/ai/providers.js +5 -5
  35. package/dist/commands/agent.d.ts +17 -0
  36. package/dist/commands/agent.js +114 -0
  37. package/dist/commands/monitor.js +50 -183
  38. package/dist/commands/new-auto.d.ts +15 -0
  39. package/dist/commands/new-auto.js +237 -0
  40. package/dist/commands/run.js +20 -10
  41. package/dist/commands/sync.d.ts +15 -0
  42. package/dist/commands/sync.js +68 -0
  43. package/dist/generator/config.d.ts +1 -41
  44. package/dist/generator/config.js +7 -0
  45. package/dist/generator/index.d.ts +2 -2
  46. package/dist/generator/templates.d.ts +2 -0
  47. package/dist/generator/templates.js +9 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +115 -4
  50. package/dist/repl/command-parser.d.ts +5 -0
  51. package/dist/repl/command-parser.js +5 -0
  52. package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
  53. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  54. package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  55. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  56. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  57. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  58. package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  59. package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
  60. package/dist/tui/app.d.ts +19 -2
  61. package/dist/tui/app.js +22 -4
  62. package/dist/tui/components/IssuePicker.d.ts +27 -0
  63. package/dist/tui/components/IssuePicker.js +64 -0
  64. package/dist/tui/components/RunCompletionSummary.js +6 -3
  65. package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
  66. package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
  67. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  68. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  69. package/dist/tui/screens/AgentScreen.d.ts +21 -0
  70. package/dist/tui/screens/AgentScreen.js +159 -0
  71. package/dist/tui/screens/InitScreen.js +4 -0
  72. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  73. package/dist/tui/screens/InterviewScreen.js +146 -10
  74. package/dist/tui/screens/MainShell.d.ts +1 -1
  75. package/dist/tui/screens/MainShell.js +36 -1
  76. package/dist/tui/screens/RunScreen.js +38 -6
  77. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  78. package/dist/tui/utils/build-run-summary.js +40 -84
  79. package/dist/tui/utils/clear-screen.d.ts +14 -0
  80. package/dist/tui/utils/clear-screen.js +16 -0
  81. package/dist/tui/utils/loop-status.d.ts +41 -1
  82. package/dist/tui/utils/loop-status.js +243 -35
  83. package/dist/tui/utils/pr-summary.d.ts +3 -2
  84. package/dist/tui/utils/pr-summary.js +41 -6
  85. package/dist/utils/config.d.ts +8 -0
  86. package/dist/utils/config.js +8 -0
  87. package/dist/utils/github.d.ts +32 -0
  88. package/dist/utils/github.js +106 -0
  89. package/package.json +4 -1
  90. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  91. package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  92. package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  93. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  94. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  95. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  96. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  97. 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
- if (value) {
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 file paths. Empty input to continue.';
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 'Enter URL, file path, or paste text (prefix with "text:" to force inline)...';
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: getGitBranch(projectRoot),
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: getGitBranch(projectRoot),
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;