wiggum-cli 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ralph.js +0 -0
- package/dist/agent/memory/ingest.d.ts +14 -0
- package/dist/agent/memory/ingest.js +77 -0
- package/dist/agent/memory/store.d.ts +15 -0
- package/dist/agent/memory/store.js +98 -0
- package/dist/agent/memory/types.d.ts +16 -0
- package/dist/agent/memory/types.js +14 -0
- package/dist/agent/orchestrator.d.ts +7 -0
- package/dist/agent/orchestrator.js +266 -0
- package/dist/agent/resolve-config.d.ts +26 -0
- package/dist/agent/resolve-config.js +43 -0
- package/dist/agent/tools/backlog.d.ts +27 -0
- package/dist/agent/tools/backlog.js +51 -0
- package/dist/agent/tools/dry-run.d.ts +106 -0
- package/dist/agent/tools/dry-run.js +119 -0
- package/dist/agent/tools/execution.d.ts +51 -0
- package/dist/agent/tools/execution.js +256 -0
- package/dist/agent/tools/feature-state.d.ts +43 -0
- package/dist/agent/tools/feature-state.js +184 -0
- package/dist/agent/tools/introspection.d.ts +23 -0
- package/dist/agent/tools/introspection.js +40 -0
- package/dist/agent/tools/memory.d.ts +44 -0
- package/dist/agent/tools/memory.js +99 -0
- package/dist/agent/tools/preflight.d.ts +7 -0
- package/dist/agent/tools/preflight.js +137 -0
- package/dist/agent/tools/reporting.d.ts +58 -0
- package/dist/agent/tools/reporting.js +119 -0
- package/dist/agent/tools/schemas.d.ts +2 -0
- package/dist/agent/tools/schemas.js +3 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/ai/conversation/conversation-manager.js +8 -0
- package/dist/ai/conversation/url-fetcher.js +27 -0
- package/dist/ai/providers.js +5 -5
- package/dist/commands/agent.d.ts +17 -0
- package/dist/commands/agent.js +114 -0
- package/dist/commands/monitor.js +50 -183
- package/dist/commands/new-auto.d.ts +15 -0
- package/dist/commands/new-auto.js +237 -0
- package/dist/commands/run.js +20 -10
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.js +68 -0
- package/dist/generator/config.d.ts +1 -41
- package/dist/generator/config.js +7 -0
- package/dist/generator/index.d.ts +2 -2
- package/dist/generator/templates.d.ts +2 -0
- package/dist/generator/templates.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -4
- package/dist/repl/command-parser.d.ts +5 -0
- package/dist/repl/command-parser.js +5 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
- package/dist/tui/app.d.ts +19 -2
- package/dist/tui/app.js +22 -4
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +64 -0
- package/dist/tui/components/RunCompletionSummary.js +6 -3
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
- package/dist/tui/orchestration/interview-orchestrator.js +27 -6
- package/dist/tui/screens/AgentScreen.d.ts +21 -0
- package/dist/tui/screens/AgentScreen.js +159 -0
- package/dist/tui/screens/InitScreen.js +4 -0
- package/dist/tui/screens/InterviewScreen.d.ts +3 -1
- package/dist/tui/screens/InterviewScreen.js +146 -10
- package/dist/tui/screens/MainShell.d.ts +1 -1
- package/dist/tui/screens/MainShell.js +36 -1
- package/dist/tui/screens/RunScreen.js +38 -6
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +40 -84
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/loop-status.d.ts +41 -1
- package/dist/tui/utils/loop-status.js +243 -35
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/github.d.ts +32 -0
- package/dist/utils/github.js +106 -0
- package/package.json +4 -1
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
|
@@ -0,0 +1,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<
|
|
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
|
-
|
|
456
|
-
|
|
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;
|