wiggum-cli 0.17.2 → 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 +58 -14
- package/dist/agent/orchestrator.d.ts +21 -3
- package/dist/agent/orchestrator.js +394 -187
- 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 +23 -4
- package/dist/agent/tools/execution.js +1 -1
- package/dist/agent/tools/introspection.js +26 -4
- 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 +100 -6
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +47 -2
- package/dist/commands/sync.js +2 -2
- package/dist/generator/config.js +13 -2
- package/dist/index.js +11 -3
- package/dist/repl/command-parser.d.ts +1 -1
- package/dist/repl/command-parser.js +1 -1
- package/dist/templates/config/ralph.config.cjs.tmpl +9 -2
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
- package/dist/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +12 -98
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +7 -0
- package/dist/templates/root/README.md.tmpl +2 -3
- package/dist/templates/scripts/feature-loop.sh.tmpl +835 -93
- package/dist/templates/scripts/loop.sh.tmpl +5 -1
- package/dist/templates/scripts/ralph-monitor.sh.tmpl +0 -2
- package/dist/tui/app.d.ts +5 -1
- package/dist/tui/app.js +22 -3
- 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 +86 -33
- package/dist/tui/hooks/useInit.d.ts +5 -1
- package/dist/tui/hooks/useInit.js +20 -2
- package/dist/tui/screens/AgentScreen.js +3 -1
- package/dist/tui/screens/InitScreen.js +12 -1
- package/dist/tui/screens/MainShell.js +70 -6
- package/dist/tui/screens/RunScreen.d.ts +6 -2
- package/dist/tui/screens/RunScreen.js +48 -6
- package/dist/tui/utils/loop-status.d.ts +15 -0
- package/dist/tui/utils/loop-status.js +89 -27
- package/dist/tui/utils/polishGoal.js +14 -1
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +14 -0
- 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/config/ralph.config.cjs.tmpl +9 -2
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
- package/src/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
- package/src/templates/prompts/PROMPT_feature.md.tmpl +12 -98
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
- package/src/templates/prompts/PROMPT_verify.md.tmpl +7 -0
- package/src/templates/root/README.md.tmpl +2 -3
- package/src/templates/scripts/feature-loop.sh.tmpl +835 -93
- package/src/templates/scripts/loop.sh.tmpl +5 -1
- package/src/templates/scripts/ralph-monitor.sh.tmpl +0 -2
|
@@ -12,12 +12,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
12
12
|
if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
|
|
13
13
|
PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
|
|
14
14
|
DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
|
|
15
|
+
CLAUDE_PERMISSION_MODE=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.claudePermissionMode || 'default')" 2>/dev/null || echo "default")
|
|
15
16
|
elif [ -f "$SCRIPT_DIR/../../ralph.config.cjs" ]; then
|
|
16
17
|
PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
|
|
17
18
|
DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
|
|
19
|
+
CLAUDE_PERMISSION_MODE=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.claudePermissionMode || 'default')" 2>/dev/null || echo "default")
|
|
18
20
|
else
|
|
19
21
|
PROMPTS_DIR=".ralph/prompts"
|
|
20
22
|
DEFAULT_MODEL="sonnet"
|
|
23
|
+
CLAUDE_PERMISSION_MODE="default"
|
|
21
24
|
fi
|
|
22
25
|
|
|
23
26
|
# Navigate to project root
|
|
@@ -31,6 +34,7 @@ ITERATION=0
|
|
|
31
34
|
echo "Starting Ralph loop"
|
|
32
35
|
echo " Prompt: $PROMPT_FILE"
|
|
33
36
|
echo " Model: $MODEL"
|
|
37
|
+
echo " Claude permission mode: $CLAUDE_PERMISSION_MODE"
|
|
34
38
|
echo " Max iterations: $MAX_ITERATIONS (0 = infinite)"
|
|
35
39
|
echo "Press Ctrl+C to stop"
|
|
36
40
|
echo ""
|
|
@@ -50,7 +54,7 @@ while true; do
|
|
|
50
54
|
ITERATION=$((ITERATION + 1))
|
|
51
55
|
echo "======================== ITERATION $ITERATION ========================"
|
|
52
56
|
|
|
53
|
-
cat "$PROMPT_FILE" | claude -p --
|
|
57
|
+
cat "$PROMPT_FILE" | claude -p --permission-mode "$CLAUDE_PERMISSION_MODE" --model "$MODEL"
|
|
54
58
|
|
|
55
59
|
echo ""
|
|
56
60
|
sleep 2
|
|
@@ -73,8 +73,6 @@ detect_phase() {
|
|
|
73
73
|
echo "Planning"
|
|
74
74
|
elif pgrep -f "PROMPT_e2e.md" > /dev/null 2>&1; then
|
|
75
75
|
echo "E2E Testing"
|
|
76
|
-
elif pgrep -f "PROMPT_verify.md" > /dev/null 2>&1; then
|
|
77
|
-
echo "Verification"
|
|
78
76
|
elif pgrep -f "PROMPT_review_manual.md" > /dev/null 2>&1; then
|
|
79
77
|
echo "PR Review"
|
|
80
78
|
elif pgrep -f "PROMPT_review_auto.md" > /dev/null 2>&1; then
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -55,7 +55,11 @@ export interface RunAppProps {
|
|
|
55
55
|
/** If true, opens in monitor-only (read-only) mode — no loop is spawned */
|
|
56
56
|
monitorOnly?: boolean;
|
|
57
57
|
/** Review mode override */
|
|
58
|
-
reviewMode?: 'manual' | 'auto';
|
|
58
|
+
reviewMode?: 'manual' | 'auto' | 'merge';
|
|
59
|
+
/** Implementation CLI override */
|
|
60
|
+
cli?: 'claude' | 'codex';
|
|
61
|
+
/** Review CLI override */
|
|
62
|
+
reviewCli?: 'claude' | 'codex';
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* Props for the main App component
|
package/dist/tui/app.js
CHANGED
|
@@ -34,7 +34,13 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
34
34
|
const [currentScreen, setCurrentScreen] = useState(initialScreen);
|
|
35
35
|
const [screenProps, setScreenProps] = useState(() => {
|
|
36
36
|
if (initialScreen === 'run' && runProps) {
|
|
37
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
featureName: runProps.featureName,
|
|
39
|
+
monitorOnly: runProps.monitorOnly,
|
|
40
|
+
reviewMode: runProps.reviewMode,
|
|
41
|
+
cli: runProps.cli,
|
|
42
|
+
reviewCli: runProps.reviewCli,
|
|
43
|
+
};
|
|
38
44
|
}
|
|
39
45
|
if (interviewProps) {
|
|
40
46
|
return { featureName: interviewProps.featureName };
|
|
@@ -49,9 +55,18 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
49
55
|
const columns = stdout?.columns ?? 80;
|
|
50
56
|
const rows = stdout?.rows ?? 24;
|
|
51
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;
|
|
52
67
|
// Shared header element - includes columns/rows in deps so the
|
|
53
68
|
// header subtree re-renders on terminal resize (banner auto-compacts)
|
|
54
|
-
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]);
|
|
55
70
|
/**
|
|
56
71
|
* Navigate to a different screen
|
|
57
72
|
*/
|
|
@@ -211,7 +226,9 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
211
226
|
return null; // useEffect will redirect to shell
|
|
212
227
|
}
|
|
213
228
|
const reviewMode = screenProps?.reviewMode;
|
|
214
|
-
|
|
229
|
+
const cli = screenProps?.cli;
|
|
230
|
+
const reviewCli = screenProps?.reviewCli;
|
|
231
|
+
return (_jsx(RunScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, sessionState: sessionState, monitorOnly: monitorOnly, reviewMode: reviewMode, cli: cli, reviewCli: reviewCli, onComplete: handleRunComplete, onBackground: handleRunBackground, onCancel: () => navigate('shell') }));
|
|
215
232
|
}
|
|
216
233
|
case 'agent': {
|
|
217
234
|
// Merge CLI-provided agentProps with navigation screenProps (from /agent command)
|
|
@@ -219,6 +236,8 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
219
236
|
...agentProps,
|
|
220
237
|
...(screenProps?.dryRun != null ? { dryRun: screenProps.dryRun } : {}),
|
|
221
238
|
...(screenProps?.maxItems != null ? { maxItems: screenProps.maxItems } : {}),
|
|
239
|
+
...(screenProps?.maxSteps != null ? { maxSteps: screenProps.maxSteps } : {}),
|
|
240
|
+
...(screenProps?.labels != null ? { labels: screenProps.labels } : {}),
|
|
222
241
|
...(screenProps?.reviewMode != null ? { reviewMode: screenProps.reviewMode } : {}),
|
|
223
242
|
...(screenProps?.issues != null ? { issues: screenProps.issues } : {}),
|
|
224
243
|
};
|
|
@@ -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;
|
|
@@ -10,7 +10,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
10
10
|
import { resolveAgentEnv } from '../../agent/resolve-config.js';
|
|
11
11
|
import { createAgentOrchestrator, } from '../../agent/orchestrator.js';
|
|
12
12
|
import { initTracing, flushTracing } from '../../utils/tracing.js';
|
|
13
|
-
import { readCurrentPhase, readLoopStatus,
|
|
13
|
+
import { readCurrentPhase, readLoopStatus, parseLoopLogDelta, parsePhaseChanges, parseImplementationPlan, getLoopLogPath, shouldSkipLine, getGitBranch, } from '../utils/loop-status.js';
|
|
14
14
|
import { getRecentCommits } from '../utils/git-summary.js';
|
|
15
15
|
const MAX_LOG_ENTRIES = 500;
|
|
16
16
|
function now() {
|
|
@@ -27,6 +27,11 @@ 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.
|
|
@@ -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,30 +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
|
-
const queueItems = issues.map((issue) => ({
|
|
211
|
-
issueNumber: (issue.number ?? issue.issueNumber),
|
|
212
|
-
title: issue.title ?? `Issue #${issue.number ?? issue.issueNumber}`,
|
|
213
|
-
labels: Array.isArray(issue.labels) ? issue.labels : [],
|
|
214
|
-
phase: 'idle',
|
|
215
|
-
}));
|
|
216
|
-
setQueue(queueItems);
|
|
217
|
-
}
|
|
218
203
|
setLogEntries((prev) => appendLog(prev, `Found ${issues.length} issue(s) in backlog`));
|
|
219
204
|
}
|
|
220
205
|
break;
|
|
221
206
|
}
|
|
222
|
-
case 'readIssue': {
|
|
223
|
-
// Update queue entry titles from full issue data (agent reads many issues during triage)
|
|
224
|
-
const issueNumber = (result?.number ?? result?.issueNumber);
|
|
225
|
-
const title = result?.title;
|
|
226
|
-
if (issueNumber && title) {
|
|
227
|
-
setQueue((prev) => prev.map((i) => i.issueNumber === issueNumber ? { ...i, title } : i));
|
|
228
|
-
}
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
207
|
case 'checkLoopStatus': {
|
|
232
208
|
const iteration = (result?.iteration ?? result?.currentIteration);
|
|
233
209
|
if (iteration != null) {
|
|
@@ -240,6 +216,78 @@ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLo
|
|
|
240
216
|
}
|
|
241
217
|
}
|
|
242
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
|
+
}
|
|
243
291
|
export function useAgentOrchestrator(options) {
|
|
244
292
|
const [status, setStatus] = useState('idle');
|
|
245
293
|
const [activeIssue, setActiveIssue] = useState(null);
|
|
@@ -253,6 +301,7 @@ export function useAgentOrchestrator(options) {
|
|
|
253
301
|
const pollingRef = useRef(null);
|
|
254
302
|
const ranLoopRef = useRef(new Set());
|
|
255
303
|
const activeIssueRef = useRef(null);
|
|
304
|
+
const completedIssuesRef = useRef(new Set());
|
|
256
305
|
// Keep ref in sync for use inside polling callback
|
|
257
306
|
useEffect(() => { activeIssueRef.current = activeIssue; }, [activeIssue]);
|
|
258
307
|
const stopLoopPolling = useCallback(() => {
|
|
@@ -268,7 +317,7 @@ export function useAgentOrchestrator(options) {
|
|
|
268
317
|
const state = {
|
|
269
318
|
featureName,
|
|
270
319
|
interval: null,
|
|
271
|
-
|
|
320
|
+
lastLogCursor: 0,
|
|
272
321
|
lastPhases: undefined,
|
|
273
322
|
};
|
|
274
323
|
// Accumulated activity events across polls
|
|
@@ -304,9 +353,10 @@ export function useAgentOrchestrator(options) {
|
|
|
304
353
|
const recentCommits = getRecentCommits(options.projectRoot, 3);
|
|
305
354
|
// Parse loop log for new events
|
|
306
355
|
const logPath = getLoopLogPath(featureName);
|
|
307
|
-
const
|
|
356
|
+
const logDelta = parseLoopLogDelta(logPath, state.lastLogCursor);
|
|
357
|
+
const logEvents = logDelta.events;
|
|
358
|
+
state.lastLogCursor = logDelta.nextCursor;
|
|
308
359
|
if (logEvents.length > 0) {
|
|
309
|
-
state.lastLogTimestamp = logEvents[logEvents.length - 1].timestamp + 1;
|
|
310
360
|
activityEventsAcc.push(...logEvents);
|
|
311
361
|
if (activityEventsAcc.length > MAX_ACTIVITY) {
|
|
312
362
|
activityEventsAcc.splice(0, activityEventsAcc.length - MAX_ACTIVITY);
|
|
@@ -390,6 +440,9 @@ export function useAgentOrchestrator(options) {
|
|
|
390
440
|
onStepUpdate: (event) => {
|
|
391
441
|
interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef);
|
|
392
442
|
},
|
|
443
|
+
onOrchestratorEvent: (event) => {
|
|
444
|
+
applyOrchestratorEvent(event, setActiveIssue, setQueue, setCompleted, setLogEntries, completedIssuesRef);
|
|
445
|
+
},
|
|
393
446
|
onProgress: (toolName, line) => {
|
|
394
447
|
// Detect generateSpec execution start (onStepFinish fires too late)
|
|
395
448
|
if (toolName === 'generateSpec') {
|
|
@@ -19,7 +19,7 @@ import type { ActionStatus } from '../components/ActionOutput.js';
|
|
|
19
19
|
/**
|
|
20
20
|
* Init workflow phases
|
|
21
21
|
*/
|
|
22
|
-
export type InitPhase = 'scanning' | 'provider-select' | 'key-input' | 'key-save' | 'model-select' | 'ai-analysis' | 'confirm' | 'generating' | 'complete' | 'error';
|
|
22
|
+
export type InitPhase = 'scanning' | 'provider-select' | 'key-input' | 'key-save' | 'model-select' | 'coding-cli-select' | 'ai-analysis' | 'confirm' | 'generating' | 'complete' | 'error';
|
|
23
23
|
/**
|
|
24
24
|
* Phase configuration for display
|
|
25
25
|
*/
|
|
@@ -66,6 +66,8 @@ export interface InitState {
|
|
|
66
66
|
provider: AIProvider | null;
|
|
67
67
|
/** Selected model */
|
|
68
68
|
model: string | null;
|
|
69
|
+
/** Selected coding CLI for generated loop config */
|
|
70
|
+
codingCli: 'claude' | 'codex';
|
|
69
71
|
/** API key entered (not persisted in state for security) */
|
|
70
72
|
hasApiKey: boolean;
|
|
71
73
|
/** Whether API key was entered this session (needs save prompt) */
|
|
@@ -109,6 +111,8 @@ export interface UseInitReturn {
|
|
|
109
111
|
setSaveKey: (save: boolean) => void;
|
|
110
112
|
/** Select model and advance to AI analysis */
|
|
111
113
|
selectModel: (model: string) => void;
|
|
114
|
+
/** Select coding CLI and advance to AI analysis */
|
|
115
|
+
selectCodingCli: (cli: 'claude' | 'codex') => void;
|
|
112
116
|
/** Set AI analysis progress */
|
|
113
117
|
setAiProgress: (status: string) => void;
|
|
114
118
|
/** Update tool call (add or update status) */
|
|
@@ -42,6 +42,11 @@ export const INIT_PHASE_CONFIGS = {
|
|
|
42
42
|
name: 'Model',
|
|
43
43
|
description: 'Select AI model',
|
|
44
44
|
},
|
|
45
|
+
'coding-cli-select': {
|
|
46
|
+
number: 3,
|
|
47
|
+
name: 'Coding CLI',
|
|
48
|
+
description: 'Select coding CLI for loops',
|
|
49
|
+
},
|
|
45
50
|
'ai-analysis': {
|
|
46
51
|
number: 4,
|
|
47
52
|
name: 'Analysis',
|
|
@@ -82,6 +87,7 @@ const initialState = {
|
|
|
82
87
|
enhancedResult: null,
|
|
83
88
|
provider: null,
|
|
84
89
|
model: null,
|
|
90
|
+
codingCli: 'claude',
|
|
85
91
|
hasApiKey: false,
|
|
86
92
|
apiKeyEnteredThisSession: false,
|
|
87
93
|
saveKeyToEnv: false,
|
|
@@ -168,12 +174,22 @@ export function useInit() {
|
|
|
168
174
|
}));
|
|
169
175
|
}, []);
|
|
170
176
|
/**
|
|
171
|
-
* Select model and advance to
|
|
177
|
+
* Select model and advance to coding CLI selection
|
|
172
178
|
*/
|
|
173
179
|
const selectModel = useCallback((model) => {
|
|
174
180
|
setState((prev) => ({
|
|
175
181
|
...prev,
|
|
176
182
|
model,
|
|
183
|
+
phase: 'coding-cli-select',
|
|
184
|
+
}));
|
|
185
|
+
}, []);
|
|
186
|
+
/**
|
|
187
|
+
* Select coding CLI and advance to AI analysis
|
|
188
|
+
*/
|
|
189
|
+
const selectCodingCli = useCallback((cli) => {
|
|
190
|
+
setState((prev) => ({
|
|
191
|
+
...prev,
|
|
192
|
+
codingCli: cli,
|
|
177
193
|
phase: 'ai-analysis',
|
|
178
194
|
isWorking: true,
|
|
179
195
|
workingStatus: 'Initializing AI analysis...',
|
|
@@ -325,7 +341,8 @@ export function useInit() {
|
|
|
325
341
|
'key-input': 'provider-select',
|
|
326
342
|
'key-save': 'key-input',
|
|
327
343
|
'model-select': prev.apiKeyEnteredThisSession ? 'key-save' : 'provider-select',
|
|
328
|
-
'
|
|
344
|
+
'coding-cli-select': 'model-select',
|
|
345
|
+
'ai-analysis': 'coding-cli-select',
|
|
329
346
|
confirm: 'ai-analysis',
|
|
330
347
|
generating: 'confirm',
|
|
331
348
|
};
|
|
@@ -345,6 +362,7 @@ export function useInit() {
|
|
|
345
362
|
setApiKey,
|
|
346
363
|
setSaveKey,
|
|
347
364
|
selectModel,
|
|
365
|
+
selectCodingCli,
|
|
348
366
|
setAiProgress,
|
|
349
367
|
updateToolCall,
|
|
350
368
|
setEnhancedResult,
|
|
@@ -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.
|
|
@@ -38,6 +38,10 @@ const PROVIDER_OPTIONS = [
|
|
|
38
38
|
{ value: 'openai', label: 'OpenAI' },
|
|
39
39
|
{ value: 'openrouter', label: 'OpenRouter', hint: 'multiple providers' },
|
|
40
40
|
];
|
|
41
|
+
const CODING_CLI_OPTIONS = [
|
|
42
|
+
{ value: 'claude', label: 'Claude Code', hint: 'default' },
|
|
43
|
+
{ value: 'codex', label: 'Codex (OpenAI)' },
|
|
44
|
+
];
|
|
41
45
|
function getModelOptions(provider) {
|
|
42
46
|
return AVAILABLE_MODELS[provider].map((m) => ({
|
|
43
47
|
value: m.value,
|
|
@@ -51,7 +55,7 @@ function getModelOptions(provider) {
|
|
|
51
55
|
* The complete Ink-based init workflow wrapped in AppShell.
|
|
52
56
|
*/
|
|
53
57
|
export function InitScreen({ header, projectRoot, sessionState, onComplete, onCancel, }) {
|
|
54
|
-
const { state, initialize, setScanResult, setExistingProvider, selectProvider, setApiKey, setSaveKey, selectModel, setAiProgress, updateToolCall, setEnhancedResult, setAiError, confirmGeneration, setGenerating, setGenerationComplete, setError, } = useInit();
|
|
58
|
+
const { state, initialize, setScanResult, setExistingProvider, selectProvider, setApiKey, setSaveKey, selectModel, selectCodingCli, setAiProgress, updateToolCall, setEnhancedResult, setAiError, confirmGeneration, setGenerating, setGenerationComplete, setError, } = useInit();
|
|
55
59
|
const apiKeyRef = useRef(null);
|
|
56
60
|
const aiAnalysisStarted = useRef(false);
|
|
57
61
|
const generationStarted = useRef(false);
|
|
@@ -189,6 +193,8 @@ export function InitScreen({ header, projectRoot, sessionState, onComplete, onCa
|
|
|
189
193
|
customVariables: {
|
|
190
194
|
...(state.provider ? { agentProvider: state.provider } : {}),
|
|
191
195
|
...(state.model ? { agentModel: state.model } : {}),
|
|
196
|
+
codingCli: state.codingCli,
|
|
197
|
+
reviewCli: state.codingCli,
|
|
192
198
|
},
|
|
193
199
|
});
|
|
194
200
|
try {
|
|
@@ -261,6 +267,9 @@ export function InitScreen({ header, projectRoot, sessionState, onComplete, onCa
|
|
|
261
267
|
const handleModelSelect = useCallback((model) => {
|
|
262
268
|
selectModel(model);
|
|
263
269
|
}, [selectModel]);
|
|
270
|
+
const handleCodingCliSelect = useCallback((cli) => {
|
|
271
|
+
selectCodingCli(cli);
|
|
272
|
+
}, [selectCodingCli]);
|
|
264
273
|
const handleConfirmGeneration = useCallback((confirmed) => {
|
|
265
274
|
confirmGeneration(confirmed);
|
|
266
275
|
}, [confirmGeneration]);
|
|
@@ -292,6 +301,8 @@ export function InitScreen({ header, projectRoot, sessionState, onComplete, onCa
|
|
|
292
301
|
if (!state.provider)
|
|
293
302
|
return null;
|
|
294
303
|
return (_jsx(Select, { message: "Select model:", options: getModelOptions(state.provider), onSelect: handleModelSelect, onCancel: onCancel }));
|
|
304
|
+
case 'coding-cli-select':
|
|
305
|
+
return (_jsx(Select, { message: "Which coding CLI do you use for loops?", options: CODING_CLI_OPTIONS, onSelect: handleCodingCliSelect, onCancel: onCancel }));
|
|
295
306
|
case 'confirm':
|
|
296
307
|
return (_jsx(Confirm, { message: "Generate Ralph configuration files?", onConfirm: handleConfirmGeneration, onCancel: onCancel, initialValue: true }));
|
|
297
308
|
default:
|
|
@@ -137,6 +137,8 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
137
137
|
}
|
|
138
138
|
// Parse optional flags, separating them from positional args
|
|
139
139
|
let reviewMode;
|
|
140
|
+
let cli;
|
|
141
|
+
let reviewCli;
|
|
140
142
|
const positional = [];
|
|
141
143
|
for (let i = 0; i < args.length; i++) {
|
|
142
144
|
if (args[i] === '--review-mode') {
|
|
@@ -146,18 +148,44 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
146
148
|
}
|
|
147
149
|
continue;
|
|
148
150
|
}
|
|
151
|
+
if (args[i] === '--cli') {
|
|
152
|
+
if (i + 1 < args.length) {
|
|
153
|
+
cli = args[i + 1];
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (args[i] === '--review-cli') {
|
|
159
|
+
if (i + 1 < args.length) {
|
|
160
|
+
reviewCli = args[i + 1];
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (args[i]?.startsWith('--')) {
|
|
166
|
+
addSystemMessage(`Unknown flag '${args[i]}' for /run.`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
149
169
|
positional.push(args[i]);
|
|
150
170
|
}
|
|
151
|
-
if (reviewMode !== undefined &&
|
|
152
|
-
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual' or '
|
|
171
|
+
if (reviewMode !== undefined && !['manual', 'auto', 'merge'].includes(reviewMode)) {
|
|
172
|
+
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual', 'auto', or 'merge'.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (cli !== undefined && !['claude', 'codex'].includes(cli)) {
|
|
176
|
+
addSystemMessage(`Invalid --cli value '${cli}'. Use 'claude' or 'codex'.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (reviewCli !== undefined && !['claude', 'codex'].includes(reviewCli)) {
|
|
180
|
+
addSystemMessage(`Invalid --review-cli value '${reviewCli}'. Use 'claude' or 'codex'.`);
|
|
153
181
|
return;
|
|
154
182
|
}
|
|
155
183
|
const featureName = positional[0];
|
|
156
184
|
if (!featureName) {
|
|
157
|
-
addSystemMessage('Feature name required. Usage: /run <feature-name> [--review-mode auto|
|
|
185
|
+
addSystemMessage('Feature name required. Usage: /run <feature-name> [--review-mode manual|auto|merge] [--cli claude|codex] [--review-cli claude|codex]');
|
|
158
186
|
return;
|
|
159
187
|
}
|
|
160
|
-
onNavigate('run', { featureName, reviewMode });
|
|
188
|
+
onNavigate('run', { featureName, reviewMode, cli, reviewCli });
|
|
161
189
|
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
162
190
|
const handleMonitor = useCallback((args) => {
|
|
163
191
|
if (args.length === 0) {
|
|
@@ -198,29 +226,65 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
198
226
|
// Parse optional flags
|
|
199
227
|
let dryRun = false;
|
|
200
228
|
let maxItems;
|
|
229
|
+
let maxSteps;
|
|
201
230
|
let reviewMode;
|
|
231
|
+
let labels;
|
|
232
|
+
let issues;
|
|
202
233
|
for (let i = 0; i < args.length; i++) {
|
|
203
234
|
if (args[i] === '--dry-run') {
|
|
204
235
|
dryRun = true;
|
|
205
236
|
}
|
|
206
237
|
else if (args[i] === '--max-items' && i + 1 < args.length) {
|
|
207
238
|
maxItems = parseInt(args[i + 1], 10);
|
|
208
|
-
if (Number.isNaN(maxItems)) {
|
|
239
|
+
if (Number.isNaN(maxItems) || maxItems < 1) {
|
|
209
240
|
addSystemMessage(`Invalid --max-items value '${args[i + 1]}'. Must be a number.`);
|
|
210
241
|
return;
|
|
211
242
|
}
|
|
212
243
|
i++;
|
|
213
244
|
}
|
|
245
|
+
else if (args[i] === '--max-steps' && i + 1 < args.length) {
|
|
246
|
+
maxSteps = parseInt(args[i + 1], 10);
|
|
247
|
+
if (Number.isNaN(maxSteps) || maxSteps < 1) {
|
|
248
|
+
addSystemMessage(`Invalid --max-steps value '${args[i + 1]}'. Must be a number.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
i++;
|
|
252
|
+
}
|
|
253
|
+
else if (args[i] === '--labels' && i + 1 < args.length) {
|
|
254
|
+
labels = args[i + 1].split(',').map(l => l.trim()).filter(Boolean);
|
|
255
|
+
if (labels.length === 0) {
|
|
256
|
+
addSystemMessage(`Invalid --labels value '${args[i + 1]}'. Use comma-separated labels.`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
i++;
|
|
260
|
+
}
|
|
261
|
+
else if (args[i] === '--issues' && i + 1 < args.length) {
|
|
262
|
+
const raw = args[i + 1];
|
|
263
|
+
const parsed = raw.split(',').map((s) => {
|
|
264
|
+
const n = parseInt(s.trim(), 10);
|
|
265
|
+
return Number.isNaN(n) || n < 1 ? null : n;
|
|
266
|
+
});
|
|
267
|
+
if (parsed.some((n) => n == null)) {
|
|
268
|
+
addSystemMessage(`Invalid --issues value '${raw}'. Use comma-separated issue numbers.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
issues = parsed;
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
214
274
|
else if (args[i] === '--review-mode' && i + 1 < args.length) {
|
|
215
275
|
reviewMode = args[i + 1];
|
|
216
276
|
i++;
|
|
217
277
|
}
|
|
278
|
+
else if (args[i]?.startsWith('--')) {
|
|
279
|
+
addSystemMessage(`Unknown flag '${args[i]}' for /agent.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
218
282
|
}
|
|
219
283
|
if (reviewMode !== undefined && !['manual', 'auto', 'merge'].includes(reviewMode)) {
|
|
220
284
|
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual', 'auto', or 'merge'.`);
|
|
221
285
|
return;
|
|
222
286
|
}
|
|
223
|
-
onNavigate('agent', { dryRun, maxItems, reviewMode });
|
|
287
|
+
onNavigate('agent', { dryRun, maxItems, maxSteps, reviewMode, labels, issues });
|
|
224
288
|
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
225
289
|
const handleIssueCommand = useCallback(async (searchQuery) => {
|
|
226
290
|
if (!sessionState.initialized) {
|