wiggum-cli 0.17.3 → 0.18.3
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/README.md +50 -12
- package/dist/agent/orchestrator.d.ts +21 -3
- package/dist/agent/orchestrator.js +394 -202
- package/dist/agent/resolve-config.js +1 -1
- package/dist/agent/scheduler.d.ts +29 -0
- package/dist/agent/scheduler.js +1149 -0
- package/dist/agent/tools/backlog.d.ts +6 -0
- package/dist/agent/tools/backlog.js +16 -1
- package/dist/agent/types.d.ts +113 -0
- package/dist/ai/conversation/url-fetcher.js +46 -13
- package/dist/ai/enhancer.js +1 -2
- package/dist/ai/providers.js +4 -4
- package/dist/commands/agent.d.ts +1 -0
- package/dist/commands/agent.js +53 -1
- package/dist/commands/config.js +8 -8
- package/dist/commands/sync.js +2 -2
- package/dist/index.js +4 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +73 -18
- package/dist/tui/app.js +10 -1
- package/dist/tui/components/HeaderContent.d.ts +4 -1
- package/dist/tui/components/HeaderContent.js +4 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +2 -1
- package/dist/tui/hooks/useAgentOrchestrator.js +83 -39
- package/dist/tui/screens/AgentScreen.js +3 -1
- package/dist/tui/utils/polishGoal.js +14 -1
- package/dist/utils/env.js +7 -1
- package/dist/utils/github.d.ts +13 -0
- package/dist/utils/github.js +63 -4
- package/dist/utils/logger.js +1 -1
- package/package.json +9 -7
- package/src/templates/scripts/feature-loop.sh.tmpl +73 -18
package/dist/tui/app.js
CHANGED
|
@@ -55,9 +55,18 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
55
55
|
const columns = stdout?.columns ?? 80;
|
|
56
56
|
const rows = stdout?.rows ?? 24;
|
|
57
57
|
const compact = rows < 20 || columns < 60;
|
|
58
|
+
const agentModelOverride = currentScreen === 'agent'
|
|
59
|
+
? (typeof screenProps?.modelOverride === 'string' ? screenProps.modelOverride : agentProps?.modelOverride)
|
|
60
|
+
: undefined;
|
|
61
|
+
const agentProviderOverride = currentScreen === 'agent'
|
|
62
|
+
? (sessionState.config?.agent.defaultProvider || sessionState.provider || undefined)
|
|
63
|
+
: undefined;
|
|
64
|
+
const effectiveAgentModel = currentScreen === 'agent'
|
|
65
|
+
? (agentModelOverride || sessionState.config?.agent.defaultModel || sessionState.model)
|
|
66
|
+
: undefined;
|
|
58
67
|
// Shared header element - includes columns/rows in deps so the
|
|
59
68
|
// header subtree re-renders on terminal resize (banner auto-compacts)
|
|
60
|
-
const headerElement = useMemo(() => (_jsx(HeaderContent, { version: version, sessionState: sessionState, backgroundRuns: backgroundRuns, compact: compact })), [version, sessionState, backgroundRuns, compact, columns, rows]);
|
|
69
|
+
const headerElement = useMemo(() => (_jsx(HeaderContent, { version: version, sessionState: sessionState, providerOverride: agentProviderOverride, modelOverride: effectiveAgentModel, backgroundRuns: backgroundRuns, compact: compact })), [version, sessionState, agentProviderOverride, effectiveAgentModel, backgroundRuns, compact, columns, rows]);
|
|
61
70
|
/**
|
|
62
71
|
* Navigate to a different screen
|
|
63
72
|
*/
|
|
@@ -15,6 +15,9 @@ export interface HeaderContentProps {
|
|
|
15
15
|
version: string;
|
|
16
16
|
/** Current session state */
|
|
17
17
|
sessionState: SessionState;
|
|
18
|
+
/** Optional provider/model display overrides for screen-specific modes */
|
|
19
|
+
providerOverride?: string;
|
|
20
|
+
modelOverride?: string;
|
|
18
21
|
/** Background runs (only active ones are displayed) */
|
|
19
22
|
backgroundRuns?: BackgroundRun[];
|
|
20
23
|
/** Use compact banner for small terminals */
|
|
@@ -25,4 +28,4 @@ export interface HeaderContentProps {
|
|
|
25
28
|
*
|
|
26
29
|
* Renders the banner and status row for the AppShell header zone.
|
|
27
30
|
*/
|
|
28
|
-
export declare function HeaderContent({ version, sessionState, backgroundRuns, compact, }: HeaderContentProps): React.ReactElement;
|
|
31
|
+
export declare function HeaderContent({ version, sessionState, providerOverride, modelOverride, backgroundRuns, compact, }: HeaderContentProps): React.ReactElement;
|
|
@@ -7,10 +7,12 @@ import { colors, theme } from '../theme.js';
|
|
|
7
7
|
*
|
|
8
8
|
* Renders the banner and status row for the AppShell header zone.
|
|
9
9
|
*/
|
|
10
|
-
export function HeaderContent({ version, sessionState, backgroundRuns, compact = false, }) {
|
|
10
|
+
export function HeaderContent({ version, sessionState, providerOverride, modelOverride, backgroundRuns, compact = false, }) {
|
|
11
11
|
const activeRuns = backgroundRuns?.filter((r) => !r.completed && !r.pollError) ?? [];
|
|
12
12
|
const errorRuns = backgroundRuns?.filter((r) => r.pollError) ?? [];
|
|
13
|
-
|
|
13
|
+
const provider = providerOverride || sessionState.provider;
|
|
14
|
+
const model = modelOverride || sessionState.model;
|
|
15
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(WiggumBanner, { compact: compact }), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: colors.pink, children: ["v", version] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), provider ? (_jsxs(Text, { color: colors.blue, children: [provider, "/", model] })) : (_jsx(Text, { color: colors.orange, children: "not configured" })), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { color: sessionState.initialized ? colors.green : colors.orange, children: sessionState.initialized ? 'Ready' : 'Not initialized' }), activeRuns.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { color: colors.green, children: [theme.chars.bulletLarge, " ", activeRuns[0].featureName, activeRuns[0].lastStatus.iteration > 0
|
|
14
16
|
? ` (${activeRuns[0].lastStatus.iteration}/${activeRuns[0].lastStatus.maxIterations || '?'})`
|
|
15
17
|
: ''] }), activeRuns.length > 1 && (_jsxs(Text, { dimColor: true, children: [" +", activeRuns.length - 1, " more"] }))] })), errorRuns.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { color: colors.orange, children: [theme.chars.bullet, " ", errorRuns[0].featureName, " (status unknown)"] })] }))] })] }));
|
|
16
18
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* calls into structured state (active issue, queue, completed, log).
|
|
7
7
|
* Exposes an abort() function for clean shutdown on q/Esc.
|
|
8
8
|
*/
|
|
9
|
-
import type { AgentIssueState, AgentLogEntry, ReviewMode } from '../../agent/types.js';
|
|
9
|
+
import type { AgentOrchestratorEvent, AgentIssueState, AgentLogEntry, ReviewMode } from '../../agent/types.js';
|
|
10
10
|
import { type LoopStatus, type TaskCounts, type ActivityEvent } from '../utils/loop-status.js';
|
|
11
11
|
import { type CommitLogEntry } from '../utils/git-summary.js';
|
|
12
12
|
export type AgentStatus = 'idle' | 'running' | 'complete' | 'error';
|
|
@@ -38,4 +38,5 @@ export interface UseAgentOrchestratorResult {
|
|
|
38
38
|
error: string | null;
|
|
39
39
|
abort: () => void;
|
|
40
40
|
}
|
|
41
|
+
export declare function applyOrchestratorEvent(event: AgentOrchestratorEvent, setActiveIssue: React.Dispatch<React.SetStateAction<AgentIssueState | null>>, setQueue: React.Dispatch<React.SetStateAction<AgentIssueState[]>>, setCompleted: React.Dispatch<React.SetStateAction<AgentIssueState[]>>, setLogEntries: React.Dispatch<React.SetStateAction<AgentLogEntry[]>>, completedIssuesRef: React.MutableRefObject<Set<number>>): void;
|
|
41
42
|
export declare function useAgentOrchestrator(options: UseAgentOrchestratorOptions): UseAgentOrchestratorResult;
|
|
@@ -27,11 +27,16 @@ function appendLog(prev, message, level = 'info') {
|
|
|
27
27
|
}
|
|
28
28
|
return next;
|
|
29
29
|
}
|
|
30
|
+
function shouldReopenCompletedIssue(issue) {
|
|
31
|
+
return issue.recommendation === 'resume_pr_phase'
|
|
32
|
+
|| issue.recommendation === 'pr_merged'
|
|
33
|
+
|| issue.recommendation === 'linked_pr_merged';
|
|
34
|
+
}
|
|
30
35
|
/**
|
|
31
36
|
* Interpret a tool call name and extract relevant info from args/results
|
|
32
37
|
* to drive TUI state transitions.
|
|
33
38
|
*/
|
|
34
|
-
function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef
|
|
39
|
+
function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef) {
|
|
35
40
|
// If a new tool call arrives while polling, the runLoop tool has finished — stop polling
|
|
36
41
|
for (const tc of event.toolCalls) {
|
|
37
42
|
if (tc.toolName !== 'runLoop' && pollingRef.current) {
|
|
@@ -188,15 +193,6 @@ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLo
|
|
|
188
193
|
break;
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
|
-
// Detect whether listIssues was called with a label filter (e.g. P0 check)
|
|
192
|
-
// so we don't overwrite the queue with a filtered subset.
|
|
193
|
-
const listIssuesHasLabelFilter = event.toolCalls.some((tc) => {
|
|
194
|
-
if (tc.toolName !== 'listIssues')
|
|
195
|
-
return false;
|
|
196
|
-
const args = tc.args;
|
|
197
|
-
const labels = args?.labels;
|
|
198
|
-
return Array.isArray(labels) && labels.length > 0;
|
|
199
|
-
});
|
|
200
196
|
// Process tool results for additional state updates
|
|
201
197
|
for (const tr of event.toolResults) {
|
|
202
198
|
const result = tr.result;
|
|
@@ -204,35 +200,10 @@ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLo
|
|
|
204
200
|
case 'listIssues': {
|
|
205
201
|
const issues = (result?.issues ?? result);
|
|
206
202
|
if (Array.isArray(issues)) {
|
|
207
|
-
// Only update queue from unfiltered listIssues calls (full backlog scan).
|
|
208
|
-
// Filtered calls (e.g. labels: ["bug"]) are P0/blocker checks — not the backlog.
|
|
209
|
-
if (!listIssuesHasLabelFilter) {
|
|
210
|
-
// When --issues is configured, defensively filter queue to only those issues
|
|
211
|
-
// (the tool should already filter, but this guards against edge cases)
|
|
212
|
-
const relevantIssues = issueNumberFilter
|
|
213
|
-
? issues.filter(i => issueNumberFilter.has((i.number ?? i.issueNumber)))
|
|
214
|
-
: issues;
|
|
215
|
-
const queueItems = relevantIssues.map((issue) => ({
|
|
216
|
-
issueNumber: (issue.number ?? issue.issueNumber),
|
|
217
|
-
title: issue.title ?? `Issue #${issue.number ?? issue.issueNumber}`,
|
|
218
|
-
labels: Array.isArray(issue.labels) ? issue.labels : [],
|
|
219
|
-
phase: 'idle',
|
|
220
|
-
}));
|
|
221
|
-
setQueue(queueItems);
|
|
222
|
-
}
|
|
223
203
|
setLogEntries((prev) => appendLog(prev, `Found ${issues.length} issue(s) in backlog`));
|
|
224
204
|
}
|
|
225
205
|
break;
|
|
226
206
|
}
|
|
227
|
-
case 'readIssue': {
|
|
228
|
-
// Update queue entry titles from full issue data (agent reads many issues during triage)
|
|
229
|
-
const issueNumber = (result?.number ?? result?.issueNumber);
|
|
230
|
-
const title = result?.title;
|
|
231
|
-
if (issueNumber && title) {
|
|
232
|
-
setQueue((prev) => prev.map((i) => i.issueNumber === issueNumber ? { ...i, title } : i));
|
|
233
|
-
}
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
207
|
case 'checkLoopStatus': {
|
|
237
208
|
const iteration = (result?.iteration ?? result?.currentIteration);
|
|
238
209
|
if (iteration != null) {
|
|
@@ -245,6 +216,78 @@ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLo
|
|
|
245
216
|
}
|
|
246
217
|
}
|
|
247
218
|
}
|
|
219
|
+
export function applyOrchestratorEvent(event, setActiveIssue, setQueue, setCompleted, setLogEntries, completedIssuesRef) {
|
|
220
|
+
switch (event.type) {
|
|
221
|
+
case 'scope_expanded':
|
|
222
|
+
setLogEntries((prev) => appendLog(prev, `Expanded scope with ${event.expansions.map(expansion => `#${expansion.issueNumber}`).join(', ')}`));
|
|
223
|
+
break;
|
|
224
|
+
case 'backlog_progress':
|
|
225
|
+
setLogEntries((prev) => appendLog(prev, event.message));
|
|
226
|
+
break;
|
|
227
|
+
case 'backlog_timing':
|
|
228
|
+
setLogEntries((prev) => appendLog(prev, `${event.phase} took ${event.durationMs}ms${event.count != null ? ` (${event.count})` : ''}`));
|
|
229
|
+
break;
|
|
230
|
+
case 'backlog_scanned':
|
|
231
|
+
setLogEntries((prev) => appendLog(prev, `Scanned ${event.total} backlog issue(s)`));
|
|
232
|
+
break;
|
|
233
|
+
case 'candidate_enriched':
|
|
234
|
+
setLogEntries((prev) => appendLog(prev, `Enriched #${event.issue.issueNumber}: ${event.issue.title}`));
|
|
235
|
+
break;
|
|
236
|
+
case 'dependencies_inferred':
|
|
237
|
+
if (event.edges.length > 0) {
|
|
238
|
+
setLogEntries((prev) => appendLog(prev, `Inferred ${event.edges.length} dependency edge(s) for #${event.issueNumber}`));
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case 'queue_ranked':
|
|
242
|
+
{
|
|
243
|
+
const resumedIssueNumbers = new Set(event.queue
|
|
244
|
+
.filter(shouldReopenCompletedIssue)
|
|
245
|
+
.map(issue => issue.issueNumber));
|
|
246
|
+
if (resumedIssueNumbers.size > 0) {
|
|
247
|
+
for (const issueNumber of resumedIssueNumbers) {
|
|
248
|
+
completedIssuesRef.current.delete(issueNumber);
|
|
249
|
+
}
|
|
250
|
+
setCompleted((prev) => prev.filter((issue) => !resumedIssueNumbers.has(issue.issueNumber)));
|
|
251
|
+
}
|
|
252
|
+
setQueue(event.queue.filter((issue) => !completedIssuesRef.current.has(issue.issueNumber)));
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case 'task_selected':
|
|
256
|
+
completedIssuesRef.current.delete(event.issue.issueNumber);
|
|
257
|
+
setCompleted((prev) => prev.filter((issue) => issue.issueNumber !== event.issue.issueNumber));
|
|
258
|
+
setActiveIssue({ ...event.issue, phase: 'planning' });
|
|
259
|
+
setQueue((prev) => prev.filter((issue) => issue.issueNumber !== event.issue.issueNumber));
|
|
260
|
+
setLogEntries((prev) => appendLog(prev, `Selected #${event.issue.issueNumber}: ${event.issue.title}`));
|
|
261
|
+
break;
|
|
262
|
+
case 'task_blocked':
|
|
263
|
+
setLogEntries((prev) => appendLog(prev, `Blocked #${event.issue.issueNumber}: ${event.issue.blockedBy?.[0]?.reason ?? event.issue.actionability ?? 'blocked'}`, 'warn'));
|
|
264
|
+
break;
|
|
265
|
+
case 'task_started':
|
|
266
|
+
setActiveIssue((prev) => ({ ...(prev ?? event.issue), ...event.issue }));
|
|
267
|
+
setLogEntries((prev) => appendLog(prev, `Started #${event.issue.issueNumber}`));
|
|
268
|
+
break;
|
|
269
|
+
case 'task_completed':
|
|
270
|
+
setCompleted((prev) => {
|
|
271
|
+
const filtered = prev.filter((issue) => issue.issueNumber !== event.issue.issueNumber);
|
|
272
|
+
return [...filtered, {
|
|
273
|
+
...event.issue,
|
|
274
|
+
error: event.outcome === 'failure' ? 'failed' : event.issue.error,
|
|
275
|
+
}];
|
|
276
|
+
});
|
|
277
|
+
if (event.outcome === 'success' || event.outcome === 'skipped') {
|
|
278
|
+
completedIssuesRef.current.add(event.issue.issueNumber);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
completedIssuesRef.current.delete(event.issue.issueNumber);
|
|
282
|
+
}
|
|
283
|
+
setQueue((prev) => prev.filter((issue) => issue.issueNumber !== event.issue.issueNumber));
|
|
284
|
+
setActiveIssue((prev) => prev?.issueNumber === event.issue.issueNumber ? null : prev);
|
|
285
|
+
setLogEntries((prev) => appendLog(prev, `${event.outcome === 'failure' ? 'Failed' : event.outcome === 'partial' ? 'Paused' : 'Completed'} #${event.issue.issueNumber} (${event.outcome})`, event.outcome === 'failure' ? 'error' : event.outcome === 'partial' ? 'warn' : 'success'));
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
248
291
|
export function useAgentOrchestrator(options) {
|
|
249
292
|
const [status, setStatus] = useState('idle');
|
|
250
293
|
const [activeIssue, setActiveIssue] = useState(null);
|
|
@@ -258,6 +301,7 @@ export function useAgentOrchestrator(options) {
|
|
|
258
301
|
const pollingRef = useRef(null);
|
|
259
302
|
const ranLoopRef = useRef(new Set());
|
|
260
303
|
const activeIssueRef = useRef(null);
|
|
304
|
+
const completedIssuesRef = useRef(new Set());
|
|
261
305
|
// Keep ref in sync for use inside polling callback
|
|
262
306
|
useEffect(() => { activeIssueRef.current = activeIssue; }, [activeIssue]);
|
|
263
307
|
const stopLoopPolling = useCallback(() => {
|
|
@@ -380,9 +424,6 @@ export function useAgentOrchestrator(options) {
|
|
|
380
424
|
model: options.modelOverride,
|
|
381
425
|
});
|
|
382
426
|
setLogEntries((prev) => appendLog(prev, `Using ${env.provider}/${env.modelId ?? 'default'} on ${env.owner}/${env.repo}`));
|
|
383
|
-
const issueFilter = options.issues?.length
|
|
384
|
-
? new Set(options.issues)
|
|
385
|
-
: undefined;
|
|
386
427
|
const agentConfig = {
|
|
387
428
|
model: env.model,
|
|
388
429
|
modelId: env.modelId,
|
|
@@ -397,7 +438,10 @@ export function useAgentOrchestrator(options) {
|
|
|
397
438
|
reviewMode: options.reviewMode,
|
|
398
439
|
dryRun: options.dryRun,
|
|
399
440
|
onStepUpdate: (event) => {
|
|
400
|
-
interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef
|
|
441
|
+
interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef);
|
|
442
|
+
},
|
|
443
|
+
onOrchestratorEvent: (event) => {
|
|
444
|
+
applyOrchestratorEvent(event, setActiveIssue, setQueue, setCompleted, setLogEntries, completedIssuesRef);
|
|
401
445
|
},
|
|
402
446
|
onProgress: (toolName, line) => {
|
|
403
447
|
// Detect generateSpec execution start (onStepFinish fires too late)
|
|
@@ -82,7 +82,9 @@ function SectionSeparator({ width }) {
|
|
|
82
82
|
* Issues panel content — shared between wide and narrow layouts
|
|
83
83
|
*/
|
|
84
84
|
function IssuesPanel({ activeIssue, queue, completed, panelWidth, }) {
|
|
85
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Active Issue" }), activeIssue ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: colors.blue, children: ["#", activeIssue.issueNumber] }), _jsxs(Text, { children: [" ", activeIssue.title] })] }), _jsxs(Text, { dimColor: true, children: [phase.active, " ", phaseLabel(activeIssue.phase, activeIssue), activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : ''] })] })) : (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "No active issue" }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Queue", _jsxs(Text, { dimColor: true, children: [" (", queue.length, ")"] })] }), queue.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "Empty" }) })) : (queue.slice(0, 5).map((issue) => (
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Active Issue" }), activeIssue ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: colors.blue, children: ["#", activeIssue.issueNumber] }), _jsxs(Text, { children: [" ", activeIssue.title] })] }), _jsxs(Text, { dimColor: true, children: [phase.active, " ", phaseLabel(activeIssue.phase, activeIssue), activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : ''] })] })) : (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "No active issue" }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Queue", _jsxs(Text, { dimColor: true, children: [" (", queue.length, ")"] })] }), queue.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "Empty" }) })) : (queue.slice(0, 5).map((issue) => (_jsxs(Box, { marginLeft: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["#", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), (issue.actionability || issue.recommendation || issue.dependsOn?.length || issue.inferredDependsOn?.length) && (_jsxs(Text, { dimColor: true, children: [issue.scopeOrigin === 'dependency' && issue.requestedBy?.length
|
|
86
|
+
? `dependency for ${issue.requestedBy.map(n => `#${n}`).join(', ')} · `
|
|
87
|
+
: '', issue.actionability ?? 'ready', issue.recommendation ? ` · ${issue.recommendation}` : '', issue.dependsOn?.length ? ` · explicit: ${issue.dependsOn.map(n => `#${n}`).join(', ')}` : '', issue.inferredDependsOn?.length ? ` · inferred: ${issue.inferredDependsOn.map(dep => `#${dep.issueNumber} (${dep.confidence})`).join(', ')}` : ''] })), issue.blockedBy?.length ? (_jsxs(Text, { color: colors.orange, children: [" blocked: ", issue.blockedBy[0].reason] })) : null] }, issue.issueNumber)))), queue.length > 5 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["...and ", queue.length - 5, " more"] }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Completed", _jsxs(Text, { dimColor: true, children: [" (", completed.length, ")"] })] }), completed.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "None yet" }) })) : (completed.slice(-5).map((issue) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: issue.error ? colors.pink : colors.green, children: issue.error ? phase.error : phase.complete }), _jsxs(Text, { dimColor: true, children: [" #", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), issue.prUrl && (_jsxs(Text, { dimColor: true, children: [" \\u2514 PR: ", issue.prUrl] }))] }, issue.issueNumber))))] }));
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Log panel content — shared between wide and narrow layouts.
|
|
@@ -72,6 +72,19 @@ function normalizeWhitespace(text) {
|
|
|
72
72
|
function looksLikeAbbreviation(fragment) {
|
|
73
73
|
return /^(e\.g|i\.e|etc|vs|mr|mrs|dr|prof|no|vol|fig|ch|approx|est)$/i.test(fragment.trim());
|
|
74
74
|
}
|
|
75
|
+
/** Remove trailing sentence punctuation without regex backtracking. */
|
|
76
|
+
function stripTrailingSentencePunctuation(text) {
|
|
77
|
+
let end = text.length;
|
|
78
|
+
while (end > 0) {
|
|
79
|
+
const char = text.charCodeAt(end - 1);
|
|
80
|
+
if (char === 46 || char === 33 || char === 63) { // . ! ?
|
|
81
|
+
end -= 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return text.slice(0, end);
|
|
87
|
+
}
|
|
75
88
|
/**
|
|
76
89
|
* Enforce single-sentence: split conservatively on `. ` boundaries, skipping
|
|
77
90
|
* abbreviation-like fragments, then return only the first sentence.
|
|
@@ -155,7 +168,7 @@ export function polishGoalSentence(text) {
|
|
|
155
168
|
// 4. Single-sentence enforcement
|
|
156
169
|
result = toOneSentence(result);
|
|
157
170
|
// Strip any trailing sentence-ending punctuation before we add our own
|
|
158
|
-
result = result
|
|
171
|
+
result = stripTrailingSentencePunctuation(result).trim();
|
|
159
172
|
// 5. Imperative verb enforcement
|
|
160
173
|
if (!IMPERATIVE_VERB_PATTERN.test(result)) {
|
|
161
174
|
// Lowercase first char before prepending to avoid "Implement The thing"
|
package/dist/utils/env.js
CHANGED
|
@@ -77,7 +77,13 @@ export function writeKeysToEnvFile(filePath, keys) {
|
|
|
77
77
|
envContent = envContent.trimEnd() + (envContent ? '\n' : '') + `${envVar}=${value}\n`;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
fs.writeFileSync(filePath, envContent);
|
|
80
|
+
fs.writeFileSync(filePath, envContent, { mode: 0o600 });
|
|
81
|
+
try {
|
|
82
|
+
fs.chmodSync(filePath, 0o600);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Best-effort hardening; ignore chmod failures on unusual filesystems.
|
|
86
|
+
}
|
|
81
87
|
}
|
|
82
88
|
/**
|
|
83
89
|
* Load known AI provider API keys from .ralph/.env.local into process.env.
|
package/dist/utils/github.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare function isGhInstalled(): Promise<boolean>;
|
|
2
2
|
export declare function _resetGhCache(): void;
|
|
3
3
|
export interface GitHubIssueDetail {
|
|
4
|
+
number: number;
|
|
4
5
|
title: string;
|
|
5
6
|
body: string;
|
|
6
7
|
labels: string[];
|
|
8
|
+
state: 'open' | 'closed';
|
|
9
|
+
createdAt: string;
|
|
7
10
|
}
|
|
8
11
|
export declare function fetchGitHubIssue(owner: string, repo: string, number: number): Promise<GitHubIssueDetail | null>;
|
|
9
12
|
export interface GitHubIssueListItem {
|
|
@@ -17,7 +20,17 @@ export interface ListIssuesResult {
|
|
|
17
20
|
issues: GitHubIssueListItem[];
|
|
18
21
|
error?: string;
|
|
19
22
|
}
|
|
23
|
+
export interface GitHubDiagnosticCheck {
|
|
24
|
+
name: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GitHubDiagnostics {
|
|
29
|
+
success: boolean;
|
|
30
|
+
checks: GitHubDiagnosticCheck[];
|
|
31
|
+
}
|
|
20
32
|
export declare function listRepoIssues(owner: string, repo: string, search?: string, limit?: number): Promise<ListIssuesResult>;
|
|
33
|
+
export declare function runGitHubDiagnostics(owner: string, repo: string, issueNumbers?: number[]): Promise<GitHubDiagnostics>;
|
|
21
34
|
export declare function detectGitHubRemote(projectRoot: string): Promise<GitHubRepo | null>;
|
|
22
35
|
export interface ParsedGitHubIssue {
|
|
23
36
|
owner: string;
|
package/dist/utils/github.js
CHANGED
|
@@ -12,6 +12,29 @@ function safeExec(cmd, args, cwd) {
|
|
|
12
12
|
});
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
function shouldRetryGhError(error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return message.includes('error connecting to api.github.com')
|
|
18
|
+
|| message.includes('Client.Timeout exceeded')
|
|
19
|
+
|| message.includes('i/o timeout')
|
|
20
|
+
|| message.includes('TLS handshake timeout');
|
|
21
|
+
}
|
|
22
|
+
async function safeExecWithRetry(cmd, args, cwd, attempts = 3) {
|
|
23
|
+
let lastError;
|
|
24
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
25
|
+
try {
|
|
26
|
+
return await safeExec(cmd, args, cwd);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
lastError = error;
|
|
30
|
+
const canRetry = cmd === 'gh' && attempt < attempts && shouldRetryGhError(error);
|
|
31
|
+
if (!canRetry)
|
|
32
|
+
break;
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 200 * attempt));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
37
|
+
}
|
|
15
38
|
let ghInstalledCache = null;
|
|
16
39
|
export async function isGhInstalled() {
|
|
17
40
|
if (ghInstalledCache !== null)
|
|
@@ -30,16 +53,19 @@ export function _resetGhCache() {
|
|
|
30
53
|
}
|
|
31
54
|
export async function fetchGitHubIssue(owner, repo, number) {
|
|
32
55
|
try {
|
|
33
|
-
const stdout = await
|
|
56
|
+
const stdout = await safeExecWithRetry('gh', [
|
|
34
57
|
'issue', 'view', String(number),
|
|
35
58
|
'--repo', `${owner}/${repo}`,
|
|
36
|
-
'--json', 'title,body,labels',
|
|
59
|
+
'--json', 'number,title,body,labels,state,createdAt',
|
|
37
60
|
]);
|
|
38
61
|
const data = JSON.parse(stdout);
|
|
39
62
|
return {
|
|
63
|
+
number: data.number ?? number,
|
|
40
64
|
title: data.title ?? '',
|
|
41
65
|
body: data.body ?? '',
|
|
42
66
|
labels: (data.labels ?? []).map((l) => l.name),
|
|
67
|
+
state: data.state?.toLowerCase?.() === 'closed' ? 'closed' : 'open',
|
|
68
|
+
createdAt: data.createdAt ?? '',
|
|
43
69
|
};
|
|
44
70
|
}
|
|
45
71
|
catch {
|
|
@@ -58,7 +84,7 @@ export async function listRepoIssues(owner, repo, search, limit = 20) {
|
|
|
58
84
|
if (search) {
|
|
59
85
|
args.push('--search', search);
|
|
60
86
|
}
|
|
61
|
-
const stdout = await
|
|
87
|
+
const stdout = await safeExecWithRetry('gh', args);
|
|
62
88
|
const data = JSON.parse(stdout);
|
|
63
89
|
const issues = data.map((item) => ({
|
|
64
90
|
number: item.number,
|
|
@@ -74,8 +100,41 @@ export async function listRepoIssues(owner, repo, search, limit = 20) {
|
|
|
74
100
|
if (msg.includes('auth') || msg.includes('login') || msg.includes('not logged')) {
|
|
75
101
|
return { issues: [], error: 'Run "gh auth login" to enable issue browsing' };
|
|
76
102
|
}
|
|
77
|
-
return { issues: [] };
|
|
103
|
+
return { issues: [], error: `GitHub issue listing failed: ${msg}` };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function runDiagnosticCheck(name, cmd, args) {
|
|
107
|
+
try {
|
|
108
|
+
await safeExecWithRetry(cmd, args);
|
|
109
|
+
return { name, ok: true, message: 'ok' };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return { name, ok: false, message };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function runGitHubDiagnostics(owner, repo, issueNumbers) {
|
|
117
|
+
const checks = [];
|
|
118
|
+
checks.push(await runDiagnosticCheck('gh version', 'gh', ['--version']));
|
|
119
|
+
checks.push(await runDiagnosticCheck('gh auth status', 'gh', ['auth', 'status']));
|
|
120
|
+
checks.push(await runDiagnosticCheck('gh issue list', 'gh', [
|
|
121
|
+
'issue', 'list',
|
|
122
|
+
'--repo', `${owner}/${repo}`,
|
|
123
|
+
'--limit', '1',
|
|
124
|
+
'--state', 'open',
|
|
125
|
+
'--json', 'number',
|
|
126
|
+
]));
|
|
127
|
+
for (const issueNumber of issueNumbers ?? []) {
|
|
128
|
+
checks.push(await runDiagnosticCheck(`gh issue view #${issueNumber}`, 'gh', [
|
|
129
|
+
'issue', 'view', String(issueNumber),
|
|
130
|
+
'--repo', `${owner}/${repo}`,
|
|
131
|
+
'--json', 'number,title',
|
|
132
|
+
]));
|
|
78
133
|
}
|
|
134
|
+
return {
|
|
135
|
+
success: checks.every(check => check.ok),
|
|
136
|
+
checks,
|
|
137
|
+
};
|
|
79
138
|
}
|
|
80
139
|
export async function detectGitHubRemote(projectRoot) {
|
|
81
140
|
try {
|
package/dist/utils/logger.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiggum-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI-
|
|
3
|
+
"version": "0.18.3",
|
|
4
|
+
"description": "AI agent CLI for spec-driven feature loops with Claude Code and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"files": [
|
|
@@ -29,15 +29,17 @@
|
|
|
29
29
|
"cli",
|
|
30
30
|
"ai-agent",
|
|
31
31
|
"autonomous-coding",
|
|
32
|
-
"ralph-loop",
|
|
33
32
|
"spec-generation",
|
|
33
|
+
"feature-specs",
|
|
34
|
+
"feature-loop",
|
|
35
|
+
"backlog-automation",
|
|
34
36
|
"claude-code",
|
|
35
37
|
"codex",
|
|
36
|
-
"ai-coding",
|
|
37
|
-
"feature-loop",
|
|
38
|
-
"code-generation",
|
|
39
38
|
"developer-tools",
|
|
40
|
-
"
|
|
39
|
+
"terminal-ui",
|
|
40
|
+
"tech-stack-detection",
|
|
41
|
+
"ralph-loop",
|
|
42
|
+
"typescript"
|
|
41
43
|
],
|
|
42
44
|
"repository": {
|
|
43
45
|
"type": "git",
|