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,453 @@
1
+ /**
2
+ * useAgentOrchestrator — React hook that bridges the agent orchestrator
3
+ * lifecycle to TUI state via callbacks.
4
+ *
5
+ * Creates the orchestrator, runs it via stream(), and interprets tool
6
+ * calls into structured state (active issue, queue, completed, log).
7
+ * Exposes an abort() function for clean shutdown on q/Esc.
8
+ */
9
+ import { useState, useEffect, useRef, useCallback } from 'react';
10
+ import { resolveAgentEnv } from '../../agent/resolve-config.js';
11
+ import { createAgentOrchestrator, } from '../../agent/orchestrator.js';
12
+ import { initTracing, flushTracing } from '../../utils/tracing.js';
13
+ import { readCurrentPhase, readLoopStatus, parseLoopLog, parsePhaseChanges, getLoopLogPath, shouldSkipLine, } from '../utils/loop-status.js';
14
+ const MAX_LOG_ENTRIES = 500;
15
+ function now() {
16
+ return new Date().toISOString();
17
+ }
18
+ const MAX_LOG_LINE_LENGTH = 120;
19
+ function appendLog(prev, message, level = 'info') {
20
+ const truncated = message.length > MAX_LOG_LINE_LENGTH
21
+ ? message.slice(0, MAX_LOG_LINE_LENGTH - 1) + '\u2026'
22
+ : message;
23
+ const next = [...prev, { timestamp: now(), message: truncated, level }];
24
+ if (next.length > MAX_LOG_ENTRIES) {
25
+ return next.slice(next.length - MAX_LOG_ENTRIES);
26
+ }
27
+ return next;
28
+ }
29
+ /**
30
+ * Interpret a tool call name and extract relevant info from args/results
31
+ * to drive TUI state transitions.
32
+ */
33
+ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef) {
34
+ // If a new tool call arrives while polling, the runLoop tool has finished — stop polling
35
+ for (const tc of event.toolCalls) {
36
+ if (tc.toolName !== 'runLoop' && pollingRef.current) {
37
+ clearInterval(pollingRef.current.interval);
38
+ pollingRef.current = null;
39
+ }
40
+ }
41
+ for (const tc of event.toolCalls) {
42
+ const args = tc.args;
43
+ switch (tc.toolName) {
44
+ case 'listIssues':
45
+ setLogEntries((prev) => appendLog(prev, 'Scanning backlog...'));
46
+ break;
47
+ case 'readIssue': {
48
+ const issueNumber = (args?.issueNumber ?? args?.number);
49
+ if (issueNumber) {
50
+ setLogEntries((prev) => appendLog(prev, `Reading #${issueNumber}`));
51
+ }
52
+ break;
53
+ }
54
+ case 'assessFeatureState': {
55
+ const issueNumber = args?.issueNumber;
56
+ setLogEntries((prev) => appendLog(prev, `Assessing feature state${issueNumber ? ` for #${issueNumber}` : ''}`));
57
+ break;
58
+ }
59
+ case 'generateSpec': {
60
+ const genFeatureName = args?.featureName;
61
+ const genIssueNumber = args?.issueNumber;
62
+ // generateSpec signals commitment — promote from queue to active
63
+ setQueue((prev) => {
64
+ const issueNum = genIssueNumber ?? activeIssueRef.current?.issueNumber;
65
+ if (issueNum) {
66
+ const queueEntry = prev.find((i) => i.issueNumber === issueNum);
67
+ setActiveIssue((currentActive) => {
68
+ if (currentActive?.issueNumber === issueNum) {
69
+ return { ...currentActive, phase: 'generating_spec', loopFeatureName: genFeatureName ?? currentActive.loopFeatureName };
70
+ }
71
+ return {
72
+ issueNumber: issueNum,
73
+ title: queueEntry?.title ?? genFeatureName ?? `Issue #${issueNum}`,
74
+ labels: queueEntry?.labels ?? [],
75
+ phase: 'generating_spec',
76
+ loopFeatureName: genFeatureName,
77
+ };
78
+ });
79
+ return prev.filter((i) => i.issueNumber !== issueNum);
80
+ }
81
+ setActiveIssue((prev) => prev ? { ...prev, phase: 'generating_spec' } : prev);
82
+ return prev;
83
+ });
84
+ setLogEntries((prev) => appendLog(prev, 'Generating spec...'));
85
+ break;
86
+ }
87
+ case 'runLoop': {
88
+ // onStepFinish: runLoop tool has completed (may have run for 10+ min).
89
+ // Phase + polling were already started by onProgress handler.
90
+ // Stop polling now that the tool has returned.
91
+ stopLoopPolling();
92
+ const featureName = args?.featureName;
93
+ const runIssueNumber = args?.issueNumber;
94
+ // runLoop signals commitment — promote from queue to active if not already
95
+ setQueue((prev) => {
96
+ const issueNum = runIssueNumber ?? activeIssueRef.current?.issueNumber;
97
+ if (issueNum) {
98
+ const queueEntry = prev.find((i) => i.issueNumber === issueNum);
99
+ setActiveIssue((currentActive) => {
100
+ if (currentActive?.issueNumber === issueNum) {
101
+ const updated = { ...currentActive, phase: 'running_loop' };
102
+ if (featureName && !currentActive.loopFeatureName)
103
+ updated.loopFeatureName = featureName;
104
+ ranLoopRef.current.add(issueNum);
105
+ return updated;
106
+ }
107
+ const entry = {
108
+ issueNumber: issueNum,
109
+ title: queueEntry?.title ?? featureName ?? `Issue #${issueNum}`,
110
+ labels: queueEntry?.labels ?? [],
111
+ phase: 'running_loop',
112
+ loopFeatureName: featureName,
113
+ };
114
+ ranLoopRef.current.add(issueNum);
115
+ return entry;
116
+ });
117
+ return prev.filter((i) => i.issueNumber !== issueNum);
118
+ }
119
+ setActiveIssue((prev) => {
120
+ if (!prev)
121
+ return prev;
122
+ const updated = { ...prev, phase: 'running_loop' };
123
+ if (featureName && !prev.loopFeatureName)
124
+ updated.loopFeatureName = featureName;
125
+ ranLoopRef.current.add(prev.issueNumber);
126
+ return updated;
127
+ });
128
+ return prev;
129
+ });
130
+ setLogEntries((prev) => appendLog(prev, 'Development loop complete'));
131
+ break;
132
+ }
133
+ case 'checkLoopStatus':
134
+ setLogEntries((prev) => appendLog(prev, 'Checking loop status...'));
135
+ break;
136
+ case 'commentOnIssue':
137
+ setActiveIssue((prev) => prev ? { ...prev, phase: 'reporting' } : prev);
138
+ setLogEntries((prev) => appendLog(prev, 'Commenting on issue'));
139
+ break;
140
+ case 'createIssue': {
141
+ const title = args?.title;
142
+ const labels = args?.labels;
143
+ const labelStr = labels?.length ? ` [${labels.join(', ')}]` : '';
144
+ setLogEntries((prev) => appendLog(prev, `Creating issue: ${title ?? 'untitled'}${labelStr}`, 'warn'));
145
+ break;
146
+ }
147
+ case 'closeIssue': {
148
+ const issueNumber = args?.issueNumber;
149
+ setLogEntries((prev) => appendLog(prev, `Closed #${issueNumber ?? '?'}`, 'success'));
150
+ break;
151
+ }
152
+ case 'reflectOnWork': {
153
+ const outcome = args?.outcome;
154
+ const reflectIssueNum = args?.issueNumber;
155
+ // Remove from ranLoopRef since we're explicitly tracking completion
156
+ if (reflectIssueNum) {
157
+ ranLoopRef.current.delete(reflectIssueNum);
158
+ }
159
+ setActiveIssue((current) => {
160
+ // Build the completed entry from activeIssue if it matches, or from args
161
+ const completedEntry = current && current.issueNumber === reflectIssueNum
162
+ ? { ...current, phase: 'reflecting', error: outcome === 'failure' ? 'failed' : undefined }
163
+ : {
164
+ issueNumber: reflectIssueNum ?? 0,
165
+ title: `Issue #${reflectIssueNum ?? '?'}`,
166
+ labels: [],
167
+ phase: 'reflecting',
168
+ error: outcome === 'failure' ? 'failed' : undefined,
169
+ };
170
+ if (completedEntry.issueNumber) {
171
+ setCompleted((prev) => {
172
+ if (prev.some((c) => c.issueNumber === completedEntry.issueNumber))
173
+ return prev;
174
+ return [...prev, completedEntry];
175
+ });
176
+ }
177
+ // Clear activeIssue only if it was the reflected issue
178
+ if (current?.issueNumber === reflectIssueNum)
179
+ return null;
180
+ return current;
181
+ });
182
+ setLogEntries((prev) => appendLog(prev, `Reflected on #${reflectIssueNum ?? '?'}: ${outcome ?? 'done'}`, outcome === 'failure' ? 'error' : 'success'));
183
+ break;
184
+ }
185
+ default:
186
+ setLogEntries((prev) => appendLog(prev, `[tool] ${tc.toolName}`));
187
+ break;
188
+ }
189
+ }
190
+ // Detect whether listIssues was called with a label filter (e.g. P0 check)
191
+ // so we don't overwrite the queue with a filtered subset.
192
+ const listIssuesHasLabelFilter = event.toolCalls.some((tc) => {
193
+ if (tc.toolName !== 'listIssues')
194
+ return false;
195
+ const args = tc.args;
196
+ const labels = args?.labels;
197
+ return Array.isArray(labels) && labels.length > 0;
198
+ });
199
+ // Process tool results for additional state updates
200
+ for (const tr of event.toolResults) {
201
+ const result = tr.result;
202
+ switch (tr.toolName) {
203
+ case 'listIssues': {
204
+ const issues = (result?.issues ?? result);
205
+ if (Array.isArray(issues)) {
206
+ // Only update queue from unfiltered listIssues calls (full backlog scan).
207
+ // Filtered calls (e.g. labels: ["bug"]) are P0/blocker checks — not the backlog.
208
+ if (!listIssuesHasLabelFilter) {
209
+ const queueItems = issues.map((issue) => ({
210
+ issueNumber: (issue.number ?? issue.issueNumber),
211
+ title: issue.title ?? `Issue #${issue.number ?? issue.issueNumber}`,
212
+ labels: Array.isArray(issue.labels) ? issue.labels : [],
213
+ phase: 'idle',
214
+ }));
215
+ setQueue(queueItems);
216
+ }
217
+ setLogEntries((prev) => appendLog(prev, `Found ${issues.length} issue(s) in backlog`));
218
+ }
219
+ break;
220
+ }
221
+ case 'readIssue': {
222
+ // Update queue entry titles from full issue data (agent reads many issues during triage)
223
+ const issueNumber = (result?.number ?? result?.issueNumber);
224
+ const title = result?.title;
225
+ if (issueNumber && title) {
226
+ setQueue((prev) => prev.map((i) => i.issueNumber === issueNumber ? { ...i, title } : i));
227
+ }
228
+ break;
229
+ }
230
+ case 'checkLoopStatus': {
231
+ const iteration = (result?.iteration ?? result?.currentIteration);
232
+ if (iteration != null) {
233
+ setActiveIssue((prev) => prev ? { ...prev, loopIterations: iteration } : prev);
234
+ }
235
+ break;
236
+ }
237
+ default:
238
+ break;
239
+ }
240
+ }
241
+ }
242
+ export function useAgentOrchestrator(options) {
243
+ const [status, setStatus] = useState('idle');
244
+ const [activeIssue, setActiveIssue] = useState(null);
245
+ const [queue, setQueue] = useState([]);
246
+ const [completed, setCompleted] = useState([]);
247
+ const [logEntries, setLogEntries] = useState([]);
248
+ const [error, setError] = useState(null);
249
+ const abortRef = useRef(null);
250
+ const startedRef = useRef(false);
251
+ const pollingRef = useRef(null);
252
+ const ranLoopRef = useRef(new Set());
253
+ const activeIssueRef = useRef(null);
254
+ // Keep ref in sync for use inside polling callback
255
+ useEffect(() => { activeIssueRef.current = activeIssue; }, [activeIssue]);
256
+ const stopLoopPolling = useCallback(() => {
257
+ if (pollingRef.current) {
258
+ clearInterval(pollingRef.current.interval);
259
+ pollingRef.current = null;
260
+ }
261
+ }, []);
262
+ const startLoopPolling = useCallback((featureName) => {
263
+ stopLoopPolling(); // clear any existing polling
264
+ const state = {
265
+ featureName,
266
+ interval: null,
267
+ lastLogTimestamp: undefined,
268
+ lastPhases: undefined,
269
+ };
270
+ const poll = () => {
271
+ // Read current phase
272
+ const phaseLabel = readCurrentPhase(featureName);
273
+ if (phaseLabel) {
274
+ setActiveIssue((prev) => prev ? { ...prev, loopPhase: phaseLabel } : prev);
275
+ }
276
+ // Read loop status for iteration count
277
+ try {
278
+ const loopStatus = readLoopStatus(featureName);
279
+ if (loopStatus.iteration > 0) {
280
+ setActiveIssue((prev) => prev ? { ...prev, loopIterations: loopStatus.iteration } : prev);
281
+ }
282
+ }
283
+ catch {
284
+ // Invalid feature name or file not ready — skip
285
+ }
286
+ // Parse loop log for new events
287
+ const logPath = getLoopLogPath(featureName);
288
+ const logEvents = parseLoopLog(logPath, state.lastLogTimestamp);
289
+ if (logEvents.length > 0) {
290
+ state.lastLogTimestamp = logEvents[logEvents.length - 1].timestamp + 1;
291
+ setLogEntries((prev) => {
292
+ let next = prev;
293
+ for (const evt of logEvents) {
294
+ next = appendLog(next, evt.message, evt.status === 'error' ? 'error' : evt.status === 'success' ? 'success' : 'info');
295
+ }
296
+ return next;
297
+ });
298
+ }
299
+ // Parse phase changes for delta events
300
+ const phaseResult = parsePhaseChanges(featureName, state.lastPhases);
301
+ if (phaseResult.currentPhases) {
302
+ state.lastPhases = phaseResult.currentPhases;
303
+ }
304
+ if (phaseResult.events.length > 0) {
305
+ setLogEntries((prev) => {
306
+ let next = prev;
307
+ for (const evt of phaseResult.events) {
308
+ next = appendLog(next, evt.message, evt.status === 'error' ? 'error' : evt.status === 'success' ? 'success' : 'info');
309
+ }
310
+ return next;
311
+ });
312
+ }
313
+ };
314
+ state.interval = setInterval(poll, 3000);
315
+ pollingRef.current = state;
316
+ // Run first poll immediately
317
+ poll();
318
+ }, [stopLoopPolling]);
319
+ const abort = useCallback(() => {
320
+ abortRef.current?.abort();
321
+ stopLoopPolling();
322
+ }, [stopLoopPolling]);
323
+ useEffect(() => {
324
+ // Prevent double-start in React strict mode
325
+ if (startedRef.current)
326
+ return;
327
+ startedRef.current = true;
328
+ const controller = new AbortController();
329
+ abortRef.current = controller;
330
+ initTracing();
331
+ async function run() {
332
+ try {
333
+ setStatus('running');
334
+ setLogEntries((prev) => appendLog(prev, 'Resolving agent environment...'));
335
+ const env = await resolveAgentEnv(options.projectRoot, {
336
+ model: options.modelOverride,
337
+ });
338
+ setLogEntries((prev) => appendLog(prev, `Using ${env.provider}/${env.modelId ?? 'default'} on ${env.owner}/${env.repo}`));
339
+ const agentConfig = {
340
+ model: env.model,
341
+ modelId: env.modelId,
342
+ provider: env.provider,
343
+ projectRoot: env.projectRoot,
344
+ owner: env.owner,
345
+ repo: env.repo,
346
+ maxSteps: options.maxSteps,
347
+ maxItems: options.maxItems,
348
+ labels: options.labels,
349
+ reviewMode: options.reviewMode,
350
+ dryRun: options.dryRun,
351
+ onStepUpdate: (event) => {
352
+ interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef);
353
+ },
354
+ onProgress: (toolName, line) => {
355
+ // Detect generateSpec execution start (onStepFinish fires too late)
356
+ if (toolName === 'generateSpec') {
357
+ setActiveIssue((prev) => {
358
+ if (!prev || prev.phase === 'generating_spec')
359
+ return prev;
360
+ return { ...prev, phase: 'generating_spec' };
361
+ });
362
+ }
363
+ // Detect runLoop execution start and initiate temp file polling
364
+ if (toolName === 'runLoop') {
365
+ if (!pollingRef.current) {
366
+ setActiveIssue((prev) => prev ? { ...prev, phase: 'running_loop' } : prev);
367
+ setLogEntries((prev) => appendLog(prev, 'Running development loop...'));
368
+ // Get feature name from activeIssue (stored during assessFeatureState)
369
+ const featureName = activeIssueRef.current?.loopFeatureName;
370
+ if (featureName) {
371
+ startLoopPolling(featureName);
372
+ }
373
+ else {
374
+ // Fallback: parse from "Ralph Loop: <feature>" stderr line
375
+ const match = line.match(/^Ralph Loop:\s*(.+)$/);
376
+ if (match) {
377
+ const parsed = match[1].trim();
378
+ setActiveIssue((prev) => prev ? { ...prev, loopFeatureName: parsed } : prev);
379
+ startLoopPolling(parsed);
380
+ }
381
+ }
382
+ }
383
+ // All runLoop stderr handled by temp file polling — suppress raw lines
384
+ return;
385
+ }
386
+ // Filter noise for other tools
387
+ if (shouldSkipLine(line))
388
+ return;
389
+ setLogEntries((prev) => appendLog(prev, line));
390
+ },
391
+ };
392
+ const agent = createAgentOrchestrator(agentConfig);
393
+ const result = await agent.stream({
394
+ prompt: 'Begin working through the backlog.',
395
+ abortSignal: controller.signal,
396
+ });
397
+ // Consume text stream (discarded — TUI shows tool activity)
398
+ for await (const _chunk of result.textStream) {
399
+ // no-op: tool calls drive the UI, not text output
400
+ }
401
+ if (!controller.signal.aborted) {
402
+ // Promote any issue that ran a loop but wasn't explicitly completed
403
+ setActiveIssue((current) => {
404
+ if (current && ranLoopRef.current.has(current.issueNumber)) {
405
+ setCompleted((prev) => {
406
+ if (prev.some((c) => c.issueNumber === current.issueNumber))
407
+ return prev;
408
+ return [...prev, { ...current, phase: 'reflecting' }];
409
+ });
410
+ return null;
411
+ }
412
+ return current;
413
+ });
414
+ setStatus('complete');
415
+ setLogEntries((prev) => appendLog(prev, 'Agent run complete', 'success'));
416
+ }
417
+ }
418
+ catch (err) {
419
+ if (controller.signal.aborted) {
420
+ // Still promote tracked issues on abort
421
+ setActiveIssue((current) => {
422
+ if (current && ranLoopRef.current.has(current.issueNumber)) {
423
+ setCompleted((prev) => {
424
+ if (prev.some((c) => c.issueNumber === current.issueNumber))
425
+ return prev;
426
+ return [...prev, { ...current, phase: 'reflecting' }];
427
+ });
428
+ return null;
429
+ }
430
+ return current;
431
+ });
432
+ setStatus('complete');
433
+ setLogEntries((prev) => appendLog(prev, 'Agent aborted by user', 'warn'));
434
+ return;
435
+ }
436
+ const message = err instanceof Error ? err.message : String(err);
437
+ setStatus('error');
438
+ setError(message);
439
+ setLogEntries((prev) => appendLog(prev, `Agent failed: ${message}`, 'error'));
440
+ }
441
+ finally {
442
+ stopLoopPolling();
443
+ await flushTracing();
444
+ }
445
+ }
446
+ run();
447
+ return () => {
448
+ controller.abort();
449
+ stopLoopPolling();
450
+ };
451
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps -- options are stable from parent
452
+ return { status, activeIssue, queue, completed, logEntries, error, abort };
453
+ }
@@ -125,7 +125,11 @@ export declare class InterviewOrchestrator {
125
125
  /**
126
126
  * Add a reference URL or file path
127
127
  */
128
- addReference(refUrl: string): Promise<void>;
128
+ addReference(refUrl: string): Promise<boolean>;
129
+ /**
130
+ * Add pre-fetched content as a reference (skips URL/file fetch)
131
+ */
132
+ addReferenceContent(content: string, source: string): void;
129
133
  /**
130
134
  * Advance to the goals phase
131
135
  * Called when user is done adding context
@@ -13,6 +13,7 @@ import { createContext7Tools, canUseContext7 } from '../../ai/tools/context7.js'
13
13
  import { existsSync } from 'node:fs';
14
14
  import { resolve, isAbsolute } from 'node:path';
15
15
  import { isUrl } from '../../ai/conversation/url-fetcher.js';
16
+ import { isGitHubIssueUrl } from '../../utils/github.js';
16
17
  import { resolveOptionLabels } from '../types/interview.js';
17
18
  /** Maximum number of interview questions before auto-completing */
18
19
  const MAX_INTERVIEW_QUESTIONS = 10;
@@ -416,11 +417,14 @@ export class InterviewOrchestrator {
416
417
  */
417
418
  async addReference(refUrl) {
418
419
  try {
419
- this.onWorkingChange(true, 'Fetching reference...');
420
420
  const trimmed = refUrl.trim();
421
+ const ghIssue = isGitHubIssueUrl(trimmed);
422
+ this.onWorkingChange(true, ghIssue
423
+ ? `Fetching GitHub issue #${ghIssue.number}...`
424
+ : 'Fetching reference...');
421
425
  if (!trimmed) {
422
426
  this.onReady();
423
- return;
427
+ return false;
424
428
  }
425
429
  const forceInline = /^text:\s*/i.test(trimmed);
426
430
  const inlinePayload = forceInline
@@ -433,7 +437,7 @@ export class InterviewOrchestrator {
433
437
  if (forceInline && !inlinePayload) {
434
438
  this.onMessage('system', 'Error: Inline context is empty after "text:"');
435
439
  this.onReady();
436
- return;
440
+ return false;
437
441
  }
438
442
  if ((forceInline || (!isUrlInput && !fileExists && isInlineCandidate)) && inlinePayload) {
439
443
  const MAX_INLINE_LENGTH = 10000;
@@ -444,24 +448,41 @@ export class InterviewOrchestrator {
444
448
  const suffix = truncated ? ' (truncated)' : '';
445
449
  this.onMessage('system', `Added inline context${suffix}: "${preview}..."`);
446
450
  this.onReady();
447
- return;
451
+ return true;
448
452
  }
449
453
  const result = await fetchContent(trimmed, this.projectRoot);
450
454
  if (result.error) {
451
455
  this.onMessage('system', `Error: ${result.error}`);
456
+ this.onReady();
457
+ return false;
452
458
  }
453
459
  else {
454
460
  this.conversation.addReference(result.content, result.source);
455
- const preview = result.content.slice(0, 100).replace(/\n/g, ' ').trim();
456
- this.onMessage('system', `Added reference from ${result.source}: "${preview}..."`);
461
+ if (result.source.startsWith('GitHub issue #')) {
462
+ const titleMatch = result.content.match(/^# (.+)/);
463
+ const title = titleMatch ? titleMatch[1] : '';
464
+ this.onMessage('system', `Added: ${result.source}${title ? ` ${title}` : ''}`);
465
+ }
466
+ else {
467
+ const preview = result.content.slice(0, 100).replace(/\n/g, ' ').trim();
468
+ this.onMessage('system', `Added reference from ${result.source}: "${preview}..."`);
469
+ }
457
470
  }
458
471
  this.onReady();
472
+ return true;
459
473
  }
460
474
  catch (error) {
461
475
  this.onError(error instanceof Error ? error.message : String(error));
462
476
  this.onReady();
477
+ return false;
463
478
  }
464
479
  }
480
+ /**
481
+ * Add pre-fetched content as a reference (skips URL/file fetch)
482
+ */
483
+ addReferenceContent(content, source) {
484
+ this.conversation.addReference(content, source);
485
+ }
465
486
  /**
466
487
  * Advance to the goals phase
467
488
  * Called when user is done adding context
@@ -0,0 +1,21 @@
1
+ /**
2
+ * AgentScreen - TUI dashboard for the autonomous agent mode
3
+ *
4
+ * Displays issue processing status and an agent log.
5
+ * Two-column layout on wide terminals (>=65 cols), single-column on narrow.
6
+ *
7
+ * Wired to the orchestrator via useAgentOrchestrator hook, which
8
+ * interprets tool calls into structured React state. Console is patched
9
+ * on mount to prevent Ink rendering corruption.
10
+ *
11
+ * Wrapped in AppShell for consistent layout with header and footer.
12
+ */
13
+ import React from 'react';
14
+ import type { AgentAppProps } from '../app.js';
15
+ export interface AgentScreenProps {
16
+ header: React.ReactNode;
17
+ projectRoot: string;
18
+ agentOptions?: AgentAppProps;
19
+ onExit?: () => void;
20
+ }
21
+ export declare function AgentScreen({ header, projectRoot, agentOptions, onExit, }: AgentScreenProps): React.ReactElement;