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.
Files changed (71) hide show
  1. package/README.md +58 -14
  2. package/dist/agent/orchestrator.d.ts +21 -3
  3. package/dist/agent/orchestrator.js +394 -187
  4. package/dist/agent/resolve-config.js +1 -1
  5. package/dist/agent/scheduler.d.ts +29 -0
  6. package/dist/agent/scheduler.js +1149 -0
  7. package/dist/agent/tools/backlog.d.ts +6 -0
  8. package/dist/agent/tools/backlog.js +23 -4
  9. package/dist/agent/tools/execution.js +1 -1
  10. package/dist/agent/tools/introspection.js +26 -4
  11. package/dist/agent/types.d.ts +113 -0
  12. package/dist/ai/conversation/url-fetcher.js +46 -13
  13. package/dist/ai/enhancer.js +1 -2
  14. package/dist/ai/providers.js +4 -4
  15. package/dist/commands/agent.d.ts +1 -0
  16. package/dist/commands/agent.js +53 -1
  17. package/dist/commands/config.js +100 -6
  18. package/dist/commands/run.d.ts +2 -0
  19. package/dist/commands/run.js +47 -2
  20. package/dist/commands/sync.js +2 -2
  21. package/dist/generator/config.js +13 -2
  22. package/dist/index.js +11 -3
  23. package/dist/repl/command-parser.d.ts +1 -1
  24. package/dist/repl/command-parser.js +1 -1
  25. package/dist/templates/config/ralph.config.cjs.tmpl +9 -2
  26. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
  27. package/dist/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
  28. package/dist/templates/prompts/PROMPT_feature.md.tmpl +12 -98
  29. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
  30. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
  31. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
  32. package/dist/templates/prompts/PROMPT_verify.md.tmpl +7 -0
  33. package/dist/templates/root/README.md.tmpl +2 -3
  34. package/dist/templates/scripts/feature-loop.sh.tmpl +835 -93
  35. package/dist/templates/scripts/loop.sh.tmpl +5 -1
  36. package/dist/templates/scripts/ralph-monitor.sh.tmpl +0 -2
  37. package/dist/tui/app.d.ts +5 -1
  38. package/dist/tui/app.js +22 -3
  39. package/dist/tui/components/HeaderContent.d.ts +4 -1
  40. package/dist/tui/components/HeaderContent.js +4 -2
  41. package/dist/tui/hooks/useAgentOrchestrator.d.ts +2 -1
  42. package/dist/tui/hooks/useAgentOrchestrator.js +86 -33
  43. package/dist/tui/hooks/useInit.d.ts +5 -1
  44. package/dist/tui/hooks/useInit.js +20 -2
  45. package/dist/tui/screens/AgentScreen.js +3 -1
  46. package/dist/tui/screens/InitScreen.js +12 -1
  47. package/dist/tui/screens/MainShell.js +70 -6
  48. package/dist/tui/screens/RunScreen.d.ts +6 -2
  49. package/dist/tui/screens/RunScreen.js +48 -6
  50. package/dist/tui/utils/loop-status.d.ts +15 -0
  51. package/dist/tui/utils/loop-status.js +89 -27
  52. package/dist/tui/utils/polishGoal.js +14 -1
  53. package/dist/utils/config.d.ts +7 -0
  54. package/dist/utils/config.js +14 -0
  55. package/dist/utils/env.js +7 -1
  56. package/dist/utils/github.d.ts +13 -0
  57. package/dist/utils/github.js +63 -4
  58. package/dist/utils/logger.js +1 -1
  59. package/package.json +9 -7
  60. package/src/templates/config/ralph.config.cjs.tmpl +9 -2
  61. package/src/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
  62. package/src/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
  63. package/src/templates/prompts/PROMPT_feature.md.tmpl +12 -98
  64. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
  65. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
  66. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
  67. package/src/templates/prompts/PROMPT_verify.md.tmpl +7 -0
  68. package/src/templates/root/README.md.tmpl +2 -3
  69. package/src/templates/scripts/feature-loop.sh.tmpl +835 -93
  70. package/src/templates/scripts/loop.sh.tmpl +5 -1
  71. 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 --dangerously-skip-permissions --model "$MODEL"
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 { featureName: runProps.featureName, monitorOnly: runProps.monitorOnly, reviewMode: runProps.reviewMode };
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
- return (_jsx(RunScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, sessionState: sessionState, monitorOnly: monitorOnly, reviewMode: reviewMode, onComplete: handleRunComplete, onBackground: handleRunBackground, onCancel: () => navigate('shell') }));
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
- 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 }), sessionState.provider ? (_jsxs(Text, { color: colors.blue, children: [sessionState.provider, "/", sessionState.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
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, parseLoopLog, parsePhaseChanges, parseImplementationPlan, getLoopLogPath, shouldSkipLine, getGitBranch, } from '../utils/loop-status.js';
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
- lastLogTimestamp: undefined,
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 logEvents = parseLoopLog(logPath, state.lastLogTimestamp);
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 AI analysis
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
- 'ai-analysis': 'model-select',
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) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["#", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }) }, issue.issueNumber)))), queue.length > 5 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["...and ", queue.length - 5, " more"] }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Completed", _jsxs(Text, { dimColor: true, children: [" (", completed.length, ")"] })] }), completed.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "None yet" }) })) : (completed.slice(-5).map((issue) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: issue.error ? colors.pink : colors.green, children: issue.error ? phase.error : phase.complete }), _jsxs(Text, { dimColor: true, children: [" #", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), issue.prUrl && (_jsxs(Text, { dimColor: true, children: [" \\u2514 PR: ", issue.prUrl] }))] }, issue.issueNumber))))] }));
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 && reviewMode !== 'manual' && reviewMode !== 'auto') {
152
- addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual' or 'auto'.`);
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|manual]');
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) {