wiggum-cli 0.16.0 → 0.17.1

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 (101) 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 +10 -0
  51. package/dist/repl/command-parser.js +10 -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 +23 -4
  62. package/dist/tui/components/IssuePicker.d.ts +27 -0
  63. package/dist/tui/components/IssuePicker.js +73 -0
  64. package/dist/tui/components/RunCompletionSummary.js +6 -3
  65. package/dist/tui/components/StatusLine.d.ts +3 -1
  66. package/dist/tui/components/StatusLine.js +2 -2
  67. package/dist/tui/hooks/useAgentOrchestrator.d.ts +40 -0
  68. package/dist/tui/hooks/useAgentOrchestrator.js +491 -0
  69. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  70. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  71. package/dist/tui/screens/AgentScreen.d.ts +24 -0
  72. package/dist/tui/screens/AgentScreen.js +209 -0
  73. package/dist/tui/screens/InitScreen.js +4 -0
  74. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  75. package/dist/tui/screens/InterviewScreen.js +146 -10
  76. package/dist/tui/screens/MainShell.d.ts +1 -1
  77. package/dist/tui/screens/MainShell.js +115 -4
  78. package/dist/tui/screens/RunScreen.js +72 -16
  79. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  80. package/dist/tui/utils/build-run-summary.js +40 -84
  81. package/dist/tui/utils/clear-screen.d.ts +14 -0
  82. package/dist/tui/utils/clear-screen.js +16 -0
  83. package/dist/tui/utils/git-summary.d.ts +1 -0
  84. package/dist/tui/utils/git-summary.js +16 -0
  85. package/dist/tui/utils/loop-status.d.ts +41 -1
  86. package/dist/tui/utils/loop-status.js +243 -35
  87. package/dist/tui/utils/pr-summary.d.ts +3 -2
  88. package/dist/tui/utils/pr-summary.js +41 -6
  89. package/dist/utils/config.d.ts +8 -0
  90. package/dist/utils/config.js +8 -0
  91. package/dist/utils/github.d.ts +32 -0
  92. package/dist/utils/github.js +106 -0
  93. package/package.json +4 -1
  94. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  95. package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  96. package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  97. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  98. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  99. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  100. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  101. package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
@@ -0,0 +1,209 @@
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, a loop monitor, and an agent log.
6
+ * Two-column layout on wide terminals (>=65 cols), single-column on narrow.
7
+ *
8
+ * When a loop is running, the right panel splits: loop monitor (top) + log (bottom).
9
+ * When no loop is running, the right panel shows only the agent log.
10
+ *
11
+ * Wired to the orchestrator via useAgentOrchestrator hook, which
12
+ * interprets tool calls into structured React state. Console is patched
13
+ * on mount to prevent Ink rendering corruption.
14
+ *
15
+ * Wrapped in AppShell for consistent layout with header and footer.
16
+ */
17
+ import { useEffect, useCallback, useState, useRef } from 'react';
18
+ import { Box, Text, useInput, useStdout } from 'ink';
19
+ import { AppShell } from '../components/AppShell.js';
20
+ import { ActivityFeed } from '../components/ActivityFeed.js';
21
+ import { colors, phase, theme } from '../theme.js';
22
+ import { useAgentOrchestrator } from '../hooks/useAgentOrchestrator.js';
23
+ import { formatNumber } from '../utils/loop-status.js';
24
+ const NARROW_BREAKPOINT = 65;
25
+ const SECTION_CHAR = '\u2500'; // ─
26
+ function phaseLabel(p, activeIssue) {
27
+ switch (p) {
28
+ case 'idle': return 'Idle';
29
+ case 'planning': return 'Planning...';
30
+ case 'generating_spec': return 'Generating spec...';
31
+ case 'running_loop': return activeIssue?.loopPhase ?? 'Running loop...';
32
+ case 'reporting': return 'Reporting...';
33
+ case 'reflecting': return 'Reflecting...';
34
+ default: return String(p);
35
+ }
36
+ }
37
+ function logLevelColor(level) {
38
+ switch (level) {
39
+ case 'info': return colors.blue;
40
+ case 'warn': return colors.orange;
41
+ case 'error': return colors.pink;
42
+ case 'success': return colors.green;
43
+ default: return colors.gray;
44
+ }
45
+ }
46
+ /**
47
+ * Build a working status string from active issue state.
48
+ * Issue number/title is already shown in Active Issue panel — just show phase.
49
+ */
50
+ function workingLabel(activeIssue) {
51
+ if (!activeIssue)
52
+ return 'Starting...';
53
+ return phaseLabel(activeIssue.phase, activeIssue);
54
+ }
55
+ /**
56
+ * Build the footer phase string from current state.
57
+ * Shows loop sub-phase and iteration instead of duplicating issue title.
58
+ */
59
+ function footerPhase(status, activeIssue, completedCount, cancelling) {
60
+ if (cancelling)
61
+ return 'Cancelling...';
62
+ if (status === 'idle')
63
+ return 'Waiting';
64
+ if (status === 'complete')
65
+ return `Done \u2014 ${completedCount} issue${completedCount === 1 ? '' : 's'} processed`;
66
+ if (status === 'error')
67
+ return 'Error';
68
+ if (!activeIssue)
69
+ return 'Starting...';
70
+ const loopDetail = activeIssue.loopPhase ? ` \u00b7 ${activeIssue.loopPhase}` : '';
71
+ const iter = activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : '';
72
+ return `#${activeIssue.issueNumber}${loopDetail}${iter}`;
73
+ }
74
+ /**
75
+ * Horizontal section separator line
76
+ */
77
+ function SectionSeparator({ width }) {
78
+ const lineWidth = Math.max(1, width - 2); // account for panel padding
79
+ return _jsx(Text, { color: colors.separator, children: SECTION_CHAR.repeat(lineWidth) });
80
+ }
81
+ /**
82
+ * Issues panel content — shared between wide and narrow layouts
83
+ */
84
+ function IssuesPanel({ activeIssue, queue, completed, panelWidth, }) {
85
+ 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))))] }));
86
+ }
87
+ /**
88
+ * Log panel content — shared between wide and narrow layouts.
89
+ * tailSize controls how many entries to show (shrinks when monitor is visible).
90
+ */
91
+ function LogPanel({ logEntries, tailSize = 20 }) {
92
+ const visible = logEntries.slice(-tailSize);
93
+ 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}`))))] }));
94
+ }
95
+ /**
96
+ * ProgressBar — inline progress indicator
97
+ */
98
+ function ProgressBar({ percent, width = 18 }) {
99
+ const safePercent = Math.max(0, Math.min(100, percent));
100
+ const filled = Math.round((safePercent / 100) * width);
101
+ const empty = Math.max(0, width - filled);
102
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: colors.green, children: '\u2588'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '\u2591'.repeat(empty) })] }));
103
+ }
104
+ function formatDuration(start) {
105
+ const seconds = Math.max(0, Math.floor((Date.now() - start) / 1000));
106
+ const mins = Math.floor(seconds / 60);
107
+ const secs = seconds % 60;
108
+ return `${mins}:${String(secs).padStart(2, '0')}`;
109
+ }
110
+ const PROGRESS_LABEL_WIDTH = 17;
111
+ const padLabel = (label) => label.padEnd(PROGRESS_LABEL_WIDTH);
112
+ /**
113
+ * Loop Monitor panel — shows progress, commits, and activity for the active loop
114
+ */
115
+ function LoopMonitorPanel({ monitor, featureName, panelWidth, }) {
116
+ const { loopStatus, tasks, branch, recentCommits, activityEvents, startTime } = monitor;
117
+ const totalTokens = loopStatus.tokensInput + loopStatus.tokensOutput + loopStatus.cacheCreate + loopStatus.cacheRead;
118
+ const totalTasks = tasks.tasksDone + tasks.tasksPending;
119
+ const totalE2e = tasks.e2eDone + tasks.e2ePending;
120
+ const totalAll = totalTasks + totalE2e;
121
+ const doneAll = tasks.tasksDone + tasks.e2eDone;
122
+ const percentTasks = totalTasks > 0 ? Math.round((tasks.tasksDone / totalTasks) * 100) : 0;
123
+ const percentE2e = totalE2e > 0 ? Math.round((tasks.e2eDone / totalE2e) * 100) : 0;
124
+ const percentAll = totalAll > 0 ? Math.round((doneAll / totalAll) * 100) : 0;
125
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: colors.yellow, children: ["Loop Monitor ", _jsxs(Text, { dimColor: true, children: ["\\u2014 ", featureName] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: loopStatus.phase }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: loopStatus.iteration }), _jsxs(Text, { dimColor: true, children: ["/", loopStatus.maxIterations || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch })] }), totalTokens > 0 && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(loopStatus.tokensInput), " out:", formatNumber(loopStatus.tokensOutput), " cache:", formatNumber(loopStatus.cacheRead), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTime)] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Implementation:') }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [String(percentTasks).padStart(3), "%"] }), _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: padLabel('E2E Tests:') }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [String(percentE2e).padStart(3), "%"] }), _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, children: [_jsx(Text, { bold: true, children: padLabel('Overall:') }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [String(percentAll).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] }), recentCommits.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Commits" }), recentCommits.map((c) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: [" ", c.hash, " ", c.title] }) }, c.hash)))] })), activityEvents.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, maxEvents: 6 })] }))] }));
126
+ }
127
+ const REVIEW_MODES = ['manual', 'auto', 'merge'];
128
+ export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
129
+ const { stdout } = useStdout();
130
+ const columns = stdout?.columns ?? 80;
131
+ const isNarrow = columns < NARROW_BREAKPOINT;
132
+ // Patch console on mount to prevent Ink rendering corruption
133
+ // from any console.* calls in the orchestrator or AI SDK.
134
+ useEffect(() => {
135
+ let restore;
136
+ import('patch-console').then((mod) => {
137
+ const patchConsole = mod.default;
138
+ restore = patchConsole(() => {
139
+ // Swallow console output — Ink renders its own UI
140
+ });
141
+ }).catch(() => {
142
+ // patch-console not available — continue without it
143
+ });
144
+ return () => {
145
+ restore?.();
146
+ };
147
+ }, []);
148
+ const { status, activeIssue, queue, completed, logEntries, loopMonitor, error, abort } = useAgentOrchestrator({
149
+ projectRoot,
150
+ modelOverride: agentOptions?.modelOverride,
151
+ maxItems: agentOptions?.maxItems,
152
+ maxSteps: agentOptions?.maxSteps,
153
+ labels: agentOptions?.labels,
154
+ reviewMode: agentOptions?.reviewMode,
155
+ dryRun: agentOptions?.dryRun,
156
+ });
157
+ // Track whether the user requested cancellation
158
+ const [cancelling, setCancelling] = useState(false);
159
+ const [reviewMode, setReviewMode] = useState(agentOptions?.reviewMode ?? 'manual');
160
+ const onExitRef = useRef(onExit);
161
+ onExitRef.current = onExit;
162
+ // When cancelling and the orchestrator finishes, exit
163
+ useEffect(() => {
164
+ if (cancelling && (status === 'complete' || status === 'error')) {
165
+ onExitRef.current?.();
166
+ }
167
+ }, [cancelling, status]);
168
+ const isWorking = status === 'running' || cancelling;
169
+ const hasLoopMonitor = loopMonitor !== null && activeIssue?.phase === 'running_loop';
170
+ useInput(useCallback((input, key) => {
171
+ if (input === 'q' || key.escape) {
172
+ if (status === 'running') {
173
+ // Signal abort — stay mounted so cleanup can propagate to subprocesses
174
+ setCancelling(true);
175
+ abort();
176
+ }
177
+ else {
178
+ // Already done — exit immediately
179
+ onExitRef.current?.();
180
+ }
181
+ return;
182
+ }
183
+ // Shift+R cycles review mode (manual → auto → merge)
184
+ if (input === 'R') {
185
+ setReviewMode((prev) => {
186
+ const idx = REVIEW_MODES.indexOf(prev);
187
+ return REVIEW_MODES[(idx + 1) % REVIEW_MODES.length];
188
+ });
189
+ }
190
+ }, [abort, status]));
191
+ const tips = cancelling
192
+ ? 'Cancelling...'
193
+ : 'q exit \u2502 Esc back \u2502 Shift+R review mode';
194
+ const footerStatus = {
195
+ action: 'Agent',
196
+ phase: footerPhase(status, activeIssue, completed.length, cancelling),
197
+ extra: `review: ${reviewMode}`,
198
+ };
199
+ if (isNarrow) {
200
+ const panelWidth = Math.max(20, columns - 4);
201
+ return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: footerStatus, 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 }) }), hasLoopMonitor && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LoopMonitorPanel, { monitor: loopMonitor, featureName: activeIssue.loopFeatureName ?? `issue-${activeIssue.issueNumber}`, panelWidth: panelWidth }) })), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries, tailSize: hasLoopMonitor ? 8 : 20 }) })] }) }));
202
+ }
203
+ // Wide layout: two-column
204
+ const leftWidth = Math.max(30, Math.floor(columns * 0.4));
205
+ const rightWidth = Math.max(30, columns - leftWidth - 3);
206
+ const leftInnerWidth = leftWidth - 4; // border (2) + paddingX (2)
207
+ const rightInnerWidth = rightWidth - 4;
208
+ return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: footerStatus, 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 }) }), _jsxs(Box, { flexDirection: "column", width: rightWidth, children: [hasLoopMonitor && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, marginBottom: 1, children: _jsx(LoopMonitorPanel, { monitor: loopMonitor, featureName: activeIssue.loopFeatureName ?? `issue-${activeIssue.issueNumber}`, panelWidth: rightInnerWidth }) })), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries, tailSize: hasLoopMonitor ? 8 : 20 }) })] })] }) }));
209
+ }
@@ -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
  */