wiggum-cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +7 -1
  2. package/bin/ralph.js +0 -0
  3. package/dist/agent/memory/ingest.d.ts +14 -0
  4. package/dist/agent/memory/ingest.js +77 -0
  5. package/dist/agent/memory/store.d.ts +15 -0
  6. package/dist/agent/memory/store.js +98 -0
  7. package/dist/agent/memory/types.d.ts +16 -0
  8. package/dist/agent/memory/types.js +14 -0
  9. package/dist/agent/orchestrator.d.ts +7 -0
  10. package/dist/agent/orchestrator.js +266 -0
  11. package/dist/agent/resolve-config.d.ts +26 -0
  12. package/dist/agent/resolve-config.js +43 -0
  13. package/dist/agent/tools/backlog.d.ts +27 -0
  14. package/dist/agent/tools/backlog.js +51 -0
  15. package/dist/agent/tools/dry-run.d.ts +106 -0
  16. package/dist/agent/tools/dry-run.js +119 -0
  17. package/dist/agent/tools/execution.d.ts +51 -0
  18. package/dist/agent/tools/execution.js +256 -0
  19. package/dist/agent/tools/feature-state.d.ts +43 -0
  20. package/dist/agent/tools/feature-state.js +184 -0
  21. package/dist/agent/tools/introspection.d.ts +23 -0
  22. package/dist/agent/tools/introspection.js +40 -0
  23. package/dist/agent/tools/memory.d.ts +44 -0
  24. package/dist/agent/tools/memory.js +99 -0
  25. package/dist/agent/tools/preflight.d.ts +7 -0
  26. package/dist/agent/tools/preflight.js +137 -0
  27. package/dist/agent/tools/reporting.d.ts +58 -0
  28. package/dist/agent/tools/reporting.js +119 -0
  29. package/dist/agent/tools/schemas.d.ts +2 -0
  30. package/dist/agent/tools/schemas.js +3 -0
  31. package/dist/agent/types.d.ts +45 -0
  32. package/dist/agent/types.js +1 -0
  33. package/dist/ai/conversation/conversation-manager.js +8 -0
  34. package/dist/ai/conversation/url-fetcher.js +27 -0
  35. package/dist/ai/providers.js +5 -5
  36. package/dist/commands/agent.d.ts +17 -0
  37. package/dist/commands/agent.js +114 -0
  38. package/dist/commands/monitor.js +50 -183
  39. package/dist/commands/new-auto.d.ts +15 -0
  40. package/dist/commands/new-auto.js +237 -0
  41. package/dist/commands/run.js +20 -10
  42. package/dist/commands/sync.d.ts +15 -0
  43. package/dist/commands/sync.js +68 -0
  44. package/dist/generator/config.d.ts +1 -41
  45. package/dist/generator/config.js +7 -0
  46. package/dist/generator/index.d.ts +2 -2
  47. package/dist/generator/templates.d.ts +3 -0
  48. package/dist/generator/templates.js +22 -1
  49. package/dist/index.d.ts +14 -1
  50. package/dist/index.js +333 -40
  51. package/dist/repl/command-parser.d.ts +5 -0
  52. package/dist/repl/command-parser.js +5 -0
  53. package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
  54. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
  55. package/dist/templates/prompts/PROMPT_feature.md.tmpl +39 -3
  56. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
  57. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  58. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
  59. package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  60. package/dist/templates/scripts/feature-loop.sh.tmpl +611 -95
  61. package/dist/tui/app.d.ts +34 -2
  62. package/dist/tui/app.js +31 -5
  63. package/dist/tui/components/ActivityFeed.d.ts +18 -0
  64. package/dist/tui/components/ActivityFeed.js +31 -0
  65. package/dist/tui/components/IssuePicker.d.ts +27 -0
  66. package/dist/tui/components/IssuePicker.js +64 -0
  67. package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
  68. package/dist/tui/components/RunCompletionSummary.js +103 -10
  69. package/dist/tui/components/SummaryBox.d.ts +4 -0
  70. package/dist/tui/components/SummaryBox.js +4 -2
  71. package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
  72. package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
  73. package/dist/tui/hooks/useBackgroundRuns.js +1 -1
  74. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  75. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  76. package/dist/tui/screens/AgentScreen.d.ts +21 -0
  77. package/dist/tui/screens/AgentScreen.js +159 -0
  78. package/dist/tui/screens/InitScreen.js +4 -0
  79. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  80. package/dist/tui/screens/InterviewScreen.js +146 -10
  81. package/dist/tui/screens/MainShell.d.ts +1 -1
  82. package/dist/tui/screens/MainShell.js +36 -1
  83. package/dist/tui/screens/RunScreen.d.ts +15 -15
  84. package/dist/tui/screens/RunScreen.js +96 -11
  85. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  86. package/dist/tui/utils/build-run-summary.js +44 -85
  87. package/dist/tui/utils/clear-screen.d.ts +14 -0
  88. package/dist/tui/utils/clear-screen.js +16 -0
  89. package/dist/tui/utils/git-summary.d.ts +13 -0
  90. package/dist/tui/utils/git-summary.js +30 -0
  91. package/dist/tui/utils/loop-status.d.ts +94 -0
  92. package/dist/tui/utils/loop-status.js +430 -10
  93. package/dist/tui/utils/pr-summary.d.ts +3 -2
  94. package/dist/tui/utils/pr-summary.js +41 -6
  95. package/dist/utils/ci.d.ts +8 -0
  96. package/dist/utils/ci.js +13 -0
  97. package/dist/utils/config.d.ts +8 -0
  98. package/dist/utils/config.js +8 -0
  99. package/dist/utils/github.d.ts +32 -0
  100. package/dist/utils/github.js +106 -0
  101. package/dist/utils/spec-names.js +5 -1
  102. package/package.json +10 -2
  103. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  104. package/src/templates/prompts/PROMPT_e2e.md.tmpl +162 -5
  105. package/src/templates/prompts/PROMPT_feature.md.tmpl +39 -3
  106. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +33 -8
  107. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  108. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +40 -10
  109. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  110. package/src/templates/scripts/feature-loop.sh.tmpl +611 -95
@@ -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]);
@@ -12,22 +12,9 @@
12
12
  * On completion, shows RunCompletionSummary inline.
13
13
  */
14
14
  import React from 'react';
15
+ import { type PhaseInfo } from '../utils/loop-status.js';
15
16
  import type { SessionState } from '../../repl/session-state.js';
16
- /**
17
- * Phase execution status and timing
18
- */
19
- export interface PhaseInfo {
20
- /** Unique phase identifier (e.g., 'planning', 'implementation') */
21
- id: string;
22
- /** Human-readable phase label */
23
- label: string;
24
- /** Phase completion status */
25
- status: 'success' | 'skipped' | 'failed';
26
- /** Duration in milliseconds, if available */
27
- durationMs?: number;
28
- /** Number of iterations in this phase (e.g., for implementation) */
29
- iterations?: number;
30
- }
17
+ export type { PhaseInfo } from '../utils/loop-status.js';
31
18
  /**
32
19
  * Iteration breakdown across different contexts
33
20
  */
@@ -61,6 +48,15 @@ export interface ChangesSummary {
61
48
  /** Whether git diff information was available */
62
49
  available: boolean;
63
50
  }
51
+ /**
52
+ * A single commit entry with hash and title
53
+ */
54
+ export interface CommitEntry {
55
+ /** Short commit hash */
56
+ hash: string;
57
+ /** Commit title (first line of message) */
58
+ title: string;
59
+ }
64
60
  /**
65
61
  * Commit information from git
66
62
  */
@@ -69,6 +65,8 @@ export interface CommitsSummary {
69
65
  fromHash?: string;
70
66
  /** Ending commit hash (short) */
71
67
  toHash?: string;
68
+ /** Full list of commits between fromHash and toHash */
69
+ commitList?: CommitEntry[];
72
70
  /** Merge type if applicable */
73
71
  mergeType?: 'squash' | 'normal' | 'none';
74
72
  /** Whether git commit information was available */
@@ -111,6 +109,8 @@ export interface RunSummary {
111
109
  tasksTotal: number;
112
110
  tokensInput: number;
113
111
  tokensOutput: number;
112
+ cacheCreate: number;
113
+ cacheRead: number;
114
114
  exitCode: number;
115
115
  /** True when the exit code was inferred (e.g. monitor mode heuristic) rather than observed directly */
116
116
  exitCodeInferred?: boolean;