wiggum-cli 0.17.2 → 0.17.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 (51) hide show
  1. package/README.md +8 -2
  2. package/dist/agent/orchestrator.d.ts +1 -1
  3. package/dist/agent/orchestrator.js +19 -4
  4. package/dist/agent/tools/backlog.js +8 -4
  5. package/dist/agent/tools/execution.js +1 -1
  6. package/dist/agent/tools/introspection.js +26 -4
  7. package/dist/commands/config.js +96 -2
  8. package/dist/commands/run.d.ts +2 -0
  9. package/dist/commands/run.js +47 -2
  10. package/dist/generator/config.js +13 -2
  11. package/dist/index.js +7 -1
  12. package/dist/repl/command-parser.d.ts +1 -1
  13. package/dist/repl/command-parser.js +1 -1
  14. package/dist/templates/config/ralph.config.cjs.tmpl +9 -2
  15. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
  16. package/dist/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
  17. package/dist/templates/prompts/PROMPT_feature.md.tmpl +12 -98
  18. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
  19. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
  20. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
  21. package/dist/templates/prompts/PROMPT_verify.md.tmpl +7 -0
  22. package/dist/templates/root/README.md.tmpl +2 -3
  23. package/dist/templates/scripts/feature-loop.sh.tmpl +777 -90
  24. package/dist/templates/scripts/loop.sh.tmpl +5 -1
  25. package/dist/templates/scripts/ralph-monitor.sh.tmpl +0 -2
  26. package/dist/tui/app.d.ts +5 -1
  27. package/dist/tui/app.js +12 -2
  28. package/dist/tui/hooks/useAgentOrchestrator.js +16 -7
  29. package/dist/tui/hooks/useInit.d.ts +5 -1
  30. package/dist/tui/hooks/useInit.js +20 -2
  31. package/dist/tui/screens/InitScreen.js +12 -1
  32. package/dist/tui/screens/MainShell.js +70 -6
  33. package/dist/tui/screens/RunScreen.d.ts +6 -2
  34. package/dist/tui/screens/RunScreen.js +48 -6
  35. package/dist/tui/utils/loop-status.d.ts +15 -0
  36. package/dist/tui/utils/loop-status.js +89 -27
  37. package/dist/utils/config.d.ts +7 -0
  38. package/dist/utils/config.js +14 -0
  39. package/package.json +1 -1
  40. package/src/templates/config/ralph.config.cjs.tmpl +9 -2
  41. package/src/templates/prompts/PROMPT_e2e.md.tmpl +16 -89
  42. package/src/templates/prompts/PROMPT_e2e_fix.md.tmpl +55 -0
  43. package/src/templates/prompts/PROMPT_feature.md.tmpl +12 -98
  44. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +52 -49
  45. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +30 -2
  46. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +59 -69
  47. package/src/templates/prompts/PROMPT_verify.md.tmpl +7 -0
  48. package/src/templates/root/README.md.tmpl +2 -3
  49. package/src/templates/scripts/feature-loop.sh.tmpl +777 -90
  50. package/src/templates/scripts/loop.sh.tmpl +5 -1
  51. 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 };
@@ -211,7 +217,9 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
211
217
  return null; // useEffect will redirect to shell
212
218
  }
213
219
  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') }));
220
+ const cli = screenProps?.cli;
221
+ const reviewCli = screenProps?.reviewCli;
222
+ 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
223
  }
216
224
  case 'agent': {
217
225
  // Merge CLI-provided agentProps with navigation screenProps (from /agent command)
@@ -219,6 +227,8 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
219
227
  ...agentProps,
220
228
  ...(screenProps?.dryRun != null ? { dryRun: screenProps.dryRun } : {}),
221
229
  ...(screenProps?.maxItems != null ? { maxItems: screenProps.maxItems } : {}),
230
+ ...(screenProps?.maxSteps != null ? { maxSteps: screenProps.maxSteps } : {}),
231
+ ...(screenProps?.labels != null ? { labels: screenProps.labels } : {}),
222
232
  ...(screenProps?.reviewMode != null ? { reviewMode: screenProps.reviewMode } : {}),
223
233
  ...(screenProps?.issues != null ? { issues: screenProps.issues } : {}),
224
234
  };
@@ -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() {
@@ -31,7 +31,7 @@ function appendLog(prev, message, level = 'info') {
31
31
  * Interpret a tool call name and extract relevant info from args/results
32
32
  * to drive TUI state transitions.
33
33
  */
34
- function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef) {
34
+ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef, issueNumberFilter) {
35
35
  // If a new tool call arrives while polling, the runLoop tool has finished — stop polling
36
36
  for (const tc of event.toolCalls) {
37
37
  if (tc.toolName !== 'runLoop' && pollingRef.current) {
@@ -207,7 +207,12 @@ function interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLo
207
207
  // Only update queue from unfiltered listIssues calls (full backlog scan).
208
208
  // Filtered calls (e.g. labels: ["bug"]) are P0/blocker checks — not the backlog.
209
209
  if (!listIssuesHasLabelFilter) {
210
- const queueItems = issues.map((issue) => ({
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) => ({
211
216
  issueNumber: (issue.number ?? issue.issueNumber),
212
217
  title: issue.title ?? `Issue #${issue.number ?? issue.issueNumber}`,
213
218
  labels: Array.isArray(issue.labels) ? issue.labels : [],
@@ -268,7 +273,7 @@ export function useAgentOrchestrator(options) {
268
273
  const state = {
269
274
  featureName,
270
275
  interval: null,
271
- lastLogTimestamp: undefined,
276
+ lastLogCursor: 0,
272
277
  lastPhases: undefined,
273
278
  };
274
279
  // Accumulated activity events across polls
@@ -304,9 +309,10 @@ export function useAgentOrchestrator(options) {
304
309
  const recentCommits = getRecentCommits(options.projectRoot, 3);
305
310
  // Parse loop log for new events
306
311
  const logPath = getLoopLogPath(featureName);
307
- const logEvents = parseLoopLog(logPath, state.lastLogTimestamp);
312
+ const logDelta = parseLoopLogDelta(logPath, state.lastLogCursor);
313
+ const logEvents = logDelta.events;
314
+ state.lastLogCursor = logDelta.nextCursor;
308
315
  if (logEvents.length > 0) {
309
- state.lastLogTimestamp = logEvents[logEvents.length - 1].timestamp + 1;
310
316
  activityEventsAcc.push(...logEvents);
311
317
  if (activityEventsAcc.length > MAX_ACTIVITY) {
312
318
  activityEventsAcc.splice(0, activityEventsAcc.length - MAX_ACTIVITY);
@@ -374,6 +380,9 @@ export function useAgentOrchestrator(options) {
374
380
  model: options.modelOverride,
375
381
  });
376
382
  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;
377
386
  const agentConfig = {
378
387
  model: env.model,
379
388
  modelId: env.modelId,
@@ -388,7 +397,7 @@ export function useAgentOrchestrator(options) {
388
397
  reviewMode: options.reviewMode,
389
398
  dryRun: options.dryRun,
390
399
  onStepUpdate: (event) => {
391
- interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef);
400
+ interpretToolCalls(event, setActiveIssue, setQueue, setCompleted, setLogEntries, pollingRef, stopLoopPolling, ranLoopRef, activeIssueRef, issueFilter);
392
401
  },
393
402
  onProgress: (toolName, line) => {
394
403
  // Detect generateSpec execution start (onStepFinish fires too late)
@@ -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,
@@ -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) {
@@ -150,10 +150,14 @@ export interface RunScreenProps {
150
150
  /** Monitor-only mode: don't spawn, just poll status */
151
151
  monitorOnly?: boolean;
152
152
  /** Override review mode from CLI flags (takes precedence over config) */
153
- reviewMode?: 'manual' | 'auto';
153
+ reviewMode?: 'manual' | 'auto' | 'merge';
154
+ /** Override implementation CLI from CLI flags (takes precedence over config) */
155
+ cli?: 'claude' | 'codex';
156
+ /** Override review CLI from CLI flags (takes precedence over config) */
157
+ reviewCli?: 'claude' | 'codex';
154
158
  onComplete: (summary: RunSummary) => void;
155
159
  /** Called when user presses Esc to background the run */
156
160
  onBackground?: (featureName: string) => void;
157
161
  onCancel: () => void;
158
162
  }
159
- export declare function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly, reviewMode: reviewModeProp, onComplete, onBackground, onCancel, }: RunScreenProps): React.ReactElement;
163
+ export declare function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly, reviewMode: reviewModeProp, cli: cliProp, reviewCli: reviewCliProp, onComplete, onBackground, onCancel, }: RunScreenProps): React.ReactElement;
@@ -33,6 +33,15 @@ import { readActionRequest, writeActionReply, cleanupActionFiles } from '../util
33
33
  const POLL_INTERVAL_MS = 2500;
34
34
  const ERROR_TAIL_LINES = 12;
35
35
  const RUN_REVIEW_MODES = ['manual', 'auto', 'merge'];
36
+ const RUN_LOOP_CLIS = ['claude', 'codex'];
37
+ const DEFAULT_CODEX_LOOP_MODEL = 'gpt-5.3-codex';
38
+ function getLoopModelLabel(defaultClaudeModel, codingCli, reviewCli) {
39
+ if (codingCli === 'codex' && reviewCli === 'codex')
40
+ return DEFAULT_CODEX_LOOP_MODEL;
41
+ if (codingCli === 'claude' && reviewCli === 'claude')
42
+ return defaultClaudeModel;
43
+ return `${defaultClaudeModel} (claude) / ${DEFAULT_CODEX_LOOP_MODEL} (codex)`;
44
+ }
36
45
  function findFeatureLoopScript(projectRoot, scriptsDir) {
37
46
  const localScript = join(projectRoot, scriptsDir, 'feature-loop.sh');
38
47
  if (existsSync(localScript)) {
@@ -48,6 +57,15 @@ function findFeatureLoopScript(projectRoot, scriptsDir) {
48
57
  }
49
58
  return null;
50
59
  }
60
+ function scriptSupportsCliFlags(scriptPath) {
61
+ try {
62
+ const script = readFileSync(scriptPath, 'utf-8');
63
+ return script.includes('--cli') && script.includes('--review-cli');
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
51
69
  function findSpecFile(projectRoot, feature, specsDir) {
52
70
  const possiblePaths = [
53
71
  join(projectRoot, specsDir, `${feature}.md`),
@@ -87,7 +105,7 @@ function readLogTail(logPath, maxLines) {
87
105
  return `[Unable to read log: ${err instanceof Error ? err.message : String(err)}]`;
88
106
  }
89
107
  }
90
- export function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly = false, reviewMode: reviewModeProp, onComplete, onBackground, onCancel, }) {
108
+ export function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly = false, reviewMode: reviewModeProp, cli: cliProp, reviewCli: reviewCliProp, onComplete, onBackground, onCancel, }) {
91
109
  const [status, setStatus] = useState(() => {
92
110
  try {
93
111
  return readLoopStatus(featureName);
@@ -115,6 +133,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
115
133
  const [baselineCommit, setBaselineCommit] = useState(null);
116
134
  const [recentCommits, setRecentCommits] = useState([]);
117
135
  const [reviewMode, setReviewMode] = useState(reviewModeProp ?? 'manual');
136
+ const [loopCli, setLoopCli] = useState('claude');
137
+ const [reviewCli, setReviewCli] = useState('claude');
118
138
  const [loopModel, setLoopModel] = useState(null);
119
139
  const childRef = useRef(null);
120
140
  const stopRequestedRef = useRef(false);
@@ -351,8 +371,12 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
351
371
  const config = sessionState.config ?? await loadConfigWithDefaults(projectRoot);
352
372
  specsDirRef.current = config.paths.specs;
353
373
  if (!cancelled) {
354
- setLoopModel(config.loop.defaultModel);
355
374
  setReviewMode((prev) => reviewModeProp ?? config.loop.reviewMode ?? prev);
375
+ const effectiveCli = (cliProp ?? config.loop.codingCli ?? 'claude');
376
+ const effectiveReviewCli = (reviewCliProp ?? config.loop.reviewCli ?? effectiveCli);
377
+ setLoopModel(getLoopModelLabel(config.loop.defaultModel, effectiveCli, effectiveReviewCli));
378
+ setLoopCli(effectiveCli);
379
+ setReviewCli(effectiveReviewCli);
356
380
  }
357
381
  }
358
382
  catch (err) {
@@ -396,7 +420,11 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
396
420
  configRootRef.current = config.paths.root;
397
421
  maxIterationsRef.current = config.loop.maxIterations;
398
422
  maxE2eAttemptsRef.current = config.loop.maxE2eAttempts;
399
- setLoopModel(config.loop.defaultModel);
423
+ const effectiveCli = (cliProp ?? config.loop.codingCli ?? 'claude');
424
+ const effectiveReviewCli = (reviewCliProp ?? config.loop.reviewCli ?? effectiveCli);
425
+ setLoopModel(getLoopModelLabel(config.loop.defaultModel, effectiveCli, effectiveReviewCli));
426
+ setLoopCli(effectiveCli);
427
+ setReviewCli(effectiveReviewCli);
400
428
  const specFile = findSpecFile(projectRoot, featureName, config.paths.specs);
401
429
  if (!specFile) {
402
430
  setError(`Spec file not found for "${featureName}". Run /new ${featureName} first.`);
@@ -409,6 +437,16 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
409
437
  setIsStarting(false);
410
438
  return;
411
439
  }
440
+ if (!RUN_LOOP_CLIS.includes(effectiveCli) || !RUN_LOOP_CLIS.includes(effectiveReviewCli)) {
441
+ setError(`Invalid CLI selection. Allowed values are: ${RUN_LOOP_CLIS.join(', ')}`);
442
+ setIsStarting(false);
443
+ return;
444
+ }
445
+ if ((effectiveCli !== 'claude' || effectiveReviewCli !== 'claude') && !scriptSupportsCliFlags(scriptPath)) {
446
+ setError('Your feature-loop.sh is outdated and does not support --cli/--review-cli. Re-run /init, then try again.');
447
+ setIsStarting(false);
448
+ return;
449
+ }
412
450
  const effectiveReviewMode = reviewModeProp ?? config.loop.reviewMode ?? 'manual';
413
451
  setReviewMode(effectiveReviewMode);
414
452
  if (effectiveReviewMode !== 'manual' && effectiveReviewMode !== 'auto' && effectiveReviewMode !== 'merge') {
@@ -428,6 +466,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
428
466
  featureName,
429
467
  String(config.loop.maxIterations),
430
468
  String(config.loop.maxE2eAttempts),
469
+ '--cli',
470
+ effectiveCli,
471
+ '--review-cli',
472
+ effectiveReviewCli,
431
473
  '--review-mode',
432
474
  effectiveReviewMode,
433
475
  ];
@@ -551,7 +593,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
551
593
  // Note: refreshStatusRef (not refreshStatus) is used inside to avoid re-spawning
552
594
  // the child process when the callback identity changes due to actionRequest updates.
553
595
  // eslint-disable-next-line react-hooks/exhaustive-deps
554
- }, [featureName, projectRoot, monitorOnly, sessionState.config]);
596
+ }, [featureName, projectRoot, monitorOnly, sessionState.config, reviewModeProp, cliProp, reviewCliProp]);
555
597
  const totalTasks = tasks.tasksDone + tasks.tasksPending;
556
598
  const totalE2e = tasks.e2eDone + tasks.e2ePending;
557
599
  const totalAll = totalTasks + totalE2e;
@@ -613,8 +655,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
613
655
  action: 'Run Loop',
614
656
  phase: phaseLine,
615
657
  path: featureName,
616
- extra: `review: ${reviewMode}`,
617
- }, children: completionSummary ? (_jsx(RunCompletionSummary, { summary: completionSummary })) : (!error && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: phaseLine }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: status.iteration }), _jsxs(Text, { dimColor: true, children: ["/", status.maxIterations || maxIterationsRef.current || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch }), loopModel && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Model: " }), _jsx(Text, { color: colors.blue, children: loopModel })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(status.tokensInput), " out:", formatNumber(status.tokensOutput), " cache:", formatNumber(status.cacheRead), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTimeRef.current)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Implementation:') }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [String(percentTasks).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('E2E Tests:') }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [String(percentE2e).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: padLabel('Overall:') }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [String(percentAll).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] }), recentCommits.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Commits" }), recentCommits.map((c) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: [" ", c.hash, " ", c.title] }) }, c.hash)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, latestCommit: baselineCommit && latestCommit && baselineCommit !== latestCommit
658
+ extra: `review: ${reviewMode} | cli: ${loopCli} | review-cli: ${reviewCli}`,
659
+ }, children: completionSummary ? (_jsx(RunCompletionSummary, { summary: completionSummary })) : (!error && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: phaseLine }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: status.iteration }), _jsxs(Text, { dimColor: true, children: ["/", status.maxIterations || maxIterationsRef.current || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch }), loopModel && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Model: " }), _jsx(Text, { color: colors.blue, children: loopModel })] })), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "CLI: " }), _jsx(Text, { color: colors.blue, children: loopCli }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { color: colors.blue, children: reviewCli })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(status.tokensInput), " out:", formatNumber(status.tokensOutput), " cache:", formatNumber(status.cacheRead), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTimeRef.current)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Implementation:') }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [String(percentTasks).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('E2E Tests:') }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [String(percentE2e).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: padLabel('Overall:') }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [String(percentAll).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] }), recentCommits.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Commits" }), recentCommits.map((c) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: [" ", c.hash, " ", c.title] }) }, c.hash)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, latestCommit: baselineCommit && latestCommit && baselineCommit !== latestCommit
618
660
  ? `${baselineCommit} \u2192 ${latestCommit}`
619
661
  : latestCommit || undefined })] })] }))) }));
620
662
  }
@@ -109,6 +109,14 @@ export interface ActivityEvent {
109
109
  /** Inferred status based on event content */
110
110
  status: 'success' | 'error' | 'in-progress';
111
111
  }
112
+ /**
113
+ * Incremental parse result for loop logs.
114
+ * `nextCursor` is a character offset for the next poll.
115
+ */
116
+ export interface LoopLogDelta {
117
+ events: ActivityEvent[];
118
+ nextCursor: number;
119
+ }
112
120
  /**
113
121
  * Returns true if a log line should be excluded from the activity feed.
114
122
  */
@@ -123,6 +131,13 @@ export declare function shouldSkipLine(line: string): boolean;
123
131
  * @param since - Optional epoch ms cutoff; only return events at or after this time.
124
132
  */
125
133
  export declare function parseLoopLog(logPath: string, since?: number): ActivityEvent[];
134
+ /**
135
+ * Incrementally parse only the new portion of a loop log.
136
+ *
137
+ * The cursor is a character offset in UTF-16 code units. If the file shrinks,
138
+ * parsing restarts from the beginning.
139
+ */
140
+ export declare function parseLoopLogDelta(logPath: string, cursor?: number): LoopLogDelta;
126
141
  /**
127
142
  * Detect phase changes by comparing current phases file to a known previous state,
128
143
  * and emit activity events for newly completed or started phases.