popeye-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +521 -125
  2. package/dist/adapters/claude.d.ts +16 -4
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +679 -33
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +55 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -0
  8. package/dist/adapters/gemini.js +318 -0
  9. package/dist/adapters/gemini.js.map +1 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +41 -7
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/auth/claude.d.ts +11 -9
  14. package/dist/auth/claude.d.ts.map +1 -1
  15. package/dist/auth/claude.js +107 -71
  16. package/dist/auth/claude.js.map +1 -1
  17. package/dist/auth/gemini.d.ts +58 -0
  18. package/dist/auth/gemini.d.ts.map +1 -0
  19. package/dist/auth/gemini.js +172 -0
  20. package/dist/auth/gemini.js.map +1 -0
  21. package/dist/auth/index.d.ts +11 -7
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -5
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/auth/keychain.d.ts +20 -7
  26. package/dist/auth/keychain.d.ts.map +1 -1
  27. package/dist/auth/keychain.js +85 -29
  28. package/dist/auth/keychain.js.map +1 -1
  29. package/dist/auth/openai.d.ts +2 -2
  30. package/dist/auth/openai.d.ts.map +1 -1
  31. package/dist/auth/openai.js +30 -32
  32. package/dist/auth/openai.js.map +1 -1
  33. package/dist/cli/interactive.d.ts.map +1 -1
  34. package/dist/cli/interactive.js +1151 -110
  35. package/dist/cli/interactive.js.map +1 -1
  36. package/dist/config/defaults.d.ts +6 -1
  37. package/dist/config/defaults.d.ts.map +1 -1
  38. package/dist/config/defaults.js +10 -2
  39. package/dist/config/defaults.js.map +1 -1
  40. package/dist/config/index.d.ts +10 -0
  41. package/dist/config/index.d.ts.map +1 -1
  42. package/dist/config/index.js +19 -0
  43. package/dist/config/index.js.map +1 -1
  44. package/dist/config/schema.d.ts +20 -0
  45. package/dist/config/schema.d.ts.map +1 -1
  46. package/dist/config/schema.js +7 -0
  47. package/dist/config/schema.js.map +1 -1
  48. package/dist/generators/python.d.ts.map +1 -1
  49. package/dist/generators/python.js +1 -0
  50. package/dist/generators/python.js.map +1 -1
  51. package/dist/generators/typescript.d.ts.map +1 -1
  52. package/dist/generators/typescript.js +1 -0
  53. package/dist/generators/typescript.js.map +1 -1
  54. package/dist/state/index.d.ts +108 -0
  55. package/dist/state/index.d.ts.map +1 -1
  56. package/dist/state/index.js +551 -4
  57. package/dist/state/index.js.map +1 -1
  58. package/dist/state/registry.d.ts +52 -0
  59. package/dist/state/registry.d.ts.map +1 -0
  60. package/dist/state/registry.js +215 -0
  61. package/dist/state/registry.js.map +1 -0
  62. package/dist/types/cli.d.ts +4 -0
  63. package/dist/types/cli.d.ts.map +1 -1
  64. package/dist/types/cli.js.map +1 -1
  65. package/dist/types/consensus.d.ts +69 -4
  66. package/dist/types/consensus.d.ts.map +1 -1
  67. package/dist/types/consensus.js +24 -3
  68. package/dist/types/consensus.js.map +1 -1
  69. package/dist/types/workflow.d.ts +55 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +16 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/auto-fix.d.ts +45 -0
  74. package/dist/workflow/auto-fix.d.ts.map +1 -0
  75. package/dist/workflow/auto-fix.js +274 -0
  76. package/dist/workflow/auto-fix.js.map +1 -0
  77. package/dist/workflow/consensus.d.ts +44 -2
  78. package/dist/workflow/consensus.d.ts.map +1 -1
  79. package/dist/workflow/consensus.js +565 -17
  80. package/dist/workflow/consensus.js.map +1 -1
  81. package/dist/workflow/execution-mode.d.ts +10 -4
  82. package/dist/workflow/execution-mode.d.ts.map +1 -1
  83. package/dist/workflow/execution-mode.js +547 -58
  84. package/dist/workflow/execution-mode.js.map +1 -1
  85. package/dist/workflow/index.d.ts +14 -2
  86. package/dist/workflow/index.d.ts.map +1 -1
  87. package/dist/workflow/index.js +69 -6
  88. package/dist/workflow/index.js.map +1 -1
  89. package/dist/workflow/milestone-workflow.d.ts +34 -0
  90. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  91. package/dist/workflow/milestone-workflow.js +414 -0
  92. package/dist/workflow/milestone-workflow.js.map +1 -0
  93. package/dist/workflow/plan-mode.d.ts +14 -1
  94. package/dist/workflow/plan-mode.d.ts.map +1 -1
  95. package/dist/workflow/plan-mode.js +589 -47
  96. package/dist/workflow/plan-mode.js.map +1 -1
  97. package/dist/workflow/plan-storage.d.ts +142 -0
  98. package/dist/workflow/plan-storage.d.ts.map +1 -0
  99. package/dist/workflow/plan-storage.js +331 -0
  100. package/dist/workflow/plan-storage.js.map +1 -0
  101. package/dist/workflow/project-verification.d.ts +37 -0
  102. package/dist/workflow/project-verification.d.ts.map +1 -0
  103. package/dist/workflow/project-verification.js +381 -0
  104. package/dist/workflow/project-verification.js.map +1 -0
  105. package/dist/workflow/task-workflow.d.ts +37 -0
  106. package/dist/workflow/task-workflow.d.ts.map +1 -0
  107. package/dist/workflow/task-workflow.js +383 -0
  108. package/dist/workflow/task-workflow.js.map +1 -0
  109. package/dist/workflow/test-runner.d.ts +1 -0
  110. package/dist/workflow/test-runner.d.ts.map +1 -1
  111. package/dist/workflow/test-runner.js +9 -5
  112. package/dist/workflow/test-runner.js.map +1 -1
  113. package/dist/workflow/ui-designer.d.ts +82 -0
  114. package/dist/workflow/ui-designer.d.ts.map +1 -0
  115. package/dist/workflow/ui-designer.js +234 -0
  116. package/dist/workflow/ui-designer.js.map +1 -0
  117. package/dist/workflow/ui-setup.d.ts +58 -0
  118. package/dist/workflow/ui-setup.d.ts.map +1 -0
  119. package/dist/workflow/ui-setup.js +685 -0
  120. package/dist/workflow/ui-setup.js.map +1 -0
  121. package/dist/workflow/ui-verification.d.ts +114 -0
  122. package/dist/workflow/ui-verification.d.ts.map +1 -0
  123. package/dist/workflow/ui-verification.js +258 -0
  124. package/dist/workflow/ui-verification.js.map +1 -0
  125. package/dist/workflow/workflow-logger.d.ts +110 -0
  126. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  127. package/dist/workflow/workflow-logger.js +267 -0
  128. package/dist/workflow/workflow-logger.js.map +1 -0
  129. package/package.json +2 -2
  130. package/src/adapters/claude.ts +815 -34
  131. package/src/adapters/gemini.ts +373 -0
  132. package/src/adapters/openai.ts +40 -7
  133. package/src/auth/claude.ts +120 -78
  134. package/src/auth/gemini.ts +207 -0
  135. package/src/auth/index.ts +28 -8
  136. package/src/auth/keychain.ts +95 -28
  137. package/src/auth/openai.ts +29 -36
  138. package/src/cli/interactive.ts +1357 -115
  139. package/src/config/defaults.ts +10 -2
  140. package/src/config/index.ts +21 -0
  141. package/src/config/schema.ts +7 -0
  142. package/src/generators/python.ts +1 -0
  143. package/src/generators/typescript.ts +1 -0
  144. package/src/state/index.ts +713 -4
  145. package/src/state/registry.ts +278 -0
  146. package/src/types/cli.ts +4 -0
  147. package/src/types/consensus.ts +65 -6
  148. package/src/types/workflow.ts +35 -0
  149. package/src/workflow/auto-fix.ts +340 -0
  150. package/src/workflow/consensus.ts +750 -16
  151. package/src/workflow/execution-mode.ts +673 -74
  152. package/src/workflow/index.ts +95 -6
  153. package/src/workflow/milestone-workflow.ts +576 -0
  154. package/src/workflow/plan-mode.ts +696 -50
  155. package/src/workflow/plan-storage.ts +482 -0
  156. package/src/workflow/project-verification.ts +471 -0
  157. package/src/workflow/task-workflow.ts +525 -0
  158. package/src/workflow/test-runner.ts +10 -5
  159. package/src/workflow/ui-designer.ts +337 -0
  160. package/src/workflow/ui-setup.ts +797 -0
  161. package/src/workflow/ui-verification.ts +357 -0
  162. package/src/workflow/workflow-logger.ts +353 -0
  163. package/tests/config/config.test.ts +1 -1
  164. package/tests/types/consensus.test.ts +3 -3
  165. package/tests/workflow/plan-mode.test.ts +213 -0
  166. package/tests/workflow/test-runner.test.ts +5 -3
@@ -4,16 +4,35 @@
4
4
  */
5
5
 
6
6
  import * as readline from 'node:readline';
7
- import { getAuthStatusForDisplay, authenticateClaude, authenticateOpenAI } from '../auth/index.js';
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import {
10
+ getAuthStatusForDisplay,
11
+ authenticateClaude,
12
+ authenticateOpenAI,
13
+ authenticateGemini,
14
+ isClaudeCLIInstalled,
15
+ checkClaudeCLIAuth,
16
+ checkGeminiAuth,
17
+ } from '../auth/index.js';
8
18
  import {
9
19
  runWorkflow,
10
20
  resumeWorkflow,
11
21
  getWorkflowStatus,
12
22
  getWorkflowSummary,
13
23
  } from '../workflow/index.js';
24
+ import {
25
+ analyzeProjectProgress,
26
+ verifyProjectCompletion,
27
+ } from '../state/index.js';
14
28
  import { generateProject } from '../generators/index.js';
15
- import { loadConfig } from '../config/index.js';
29
+ import {
30
+ discoverProjects,
31
+ formatProjectForDisplay,
32
+ } from '../state/registry.js';
33
+ import { loadConfig, saveConfig } from '../config/index.js';
16
34
  import type { ProjectSpec, OutputLanguage, OpenAIModel } from '../types/project.js';
35
+ import type { AIProvider, GeminiModel } from '../types/consensus.js';
17
36
  import {
18
37
  printSuccess,
19
38
  printError,
@@ -27,6 +46,8 @@ import {
27
46
  theme,
28
47
  } from './output.js';
29
48
 
49
+ // Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
50
+
30
51
  /**
31
52
  * Box drawing characters for Claude Code-style UI
32
53
  */
@@ -48,8 +69,13 @@ interface SessionState {
48
69
  projectDir: string | null;
49
70
  language: OutputLanguage;
50
71
  model: OpenAIModel;
72
+ geminiModel: GeminiModel;
51
73
  claudeAuth: boolean;
52
74
  openaiAuth: boolean;
75
+ geminiAuth: boolean;
76
+ reviewer: AIProvider;
77
+ arbitrator: AIProvider;
78
+ enableArbitration: boolean;
53
79
  }
54
80
 
55
81
  /**
@@ -65,7 +91,7 @@ function getTerminalWidth(): number {
65
91
  function drawHeader(): void {
66
92
  const width = getTerminalWidth();
67
93
  const title = ' Popeye CLI ';
68
- const subtitle = ' AI-Powered Code Generation ';
94
+ const subtitle = ' Autonomous Code Generation with AI Consensus ';
69
95
 
70
96
  // Top border
71
97
  console.log(theme.dim(box.topLeft + box.horizontal.repeat(width - 2) + box.topRight));
@@ -95,38 +121,57 @@ function drawHeader(): void {
95
121
  }
96
122
 
97
123
  /**
98
- * Draw the input box frame
124
+ * Draw hints line and top of input box
99
125
  */
100
- function drawInputFrame(state: SessionState): void {
101
- const width = getTerminalWidth();
102
-
103
- // Status items
104
- const langStatus = `${state.language}`;
105
- const modelStatus = `${state.model}`;
106
- const authStatus = state.claudeAuth && state.openaiAuth ? '' : '○';
107
- const authColor = state.claudeAuth && state.openaiAuth ? theme.success : theme.warning;
108
-
109
- // Build status line
110
- const statusItems = [
111
- theme.dim('lang:') + theme.primary(langStatus),
112
- theme.dim('model:') + theme.secondary(modelStatus),
113
- authColor(authStatus) + theme.dim(' auth'),
126
+ function drawInputBoxTop(state: SessionState): void {
127
+ const width = Math.min(getTerminalWidth(), 100);
128
+
129
+ // Hints line (above the box)
130
+ const hints = [
131
+ theme.dim('/lang ') + theme.primary('py') + theme.dim('|') + theme.primary('ts'),
132
+ theme.dim('/config'),
133
+ theme.dim('/help'),
134
+ theme.dim('/exit'),
135
+ ];
136
+ console.log(' ' + hints.join(' '));
137
+
138
+ // Status items for the top line
139
+ const langStatus = state.language;
140
+ const reviewerStatus = state.reviewer === 'openai' ? 'O' : 'G';
141
+ const arbitratorStatus = state.enableArbitration ? (state.arbitrator === 'openai' ? 'O' : 'G') : '-';
142
+ const allAuth = state.claudeAuth && state.openaiAuth && (state.enableArbitration ? state.geminiAuth : true);
143
+ const authIcon = allAuth ? '●' : '○';
144
+ const authColor = allAuth ? theme.success : theme.warning;
145
+
146
+ // Build status text
147
+ const statusParts = [
148
+ theme.primary(langStatus),
149
+ theme.dim('R:') + theme.secondary(reviewerStatus),
150
+ theme.dim('A:') + theme.secondary(arbitratorStatus),
151
+ authColor(authIcon),
114
152
  ];
115
- const statusText = statusItems.join(theme.dim(' │ '));
153
+ const statusText = statusParts.join(theme.dim(' │ '));
116
154
 
117
155
  // Calculate visible length (without ANSI codes)
118
156
  // eslint-disable-next-line no-control-regex
119
157
  const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*m/g, '');
120
158
  const statusLen = stripAnsi(statusText).length;
121
159
 
122
- // Top line with status
123
- const topLine = box.topLeft +
124
- box.horizontal.repeat(2) +
125
- ' ' + stripAnsi(statusText) + ' ' +
126
- box.horizontal.repeat(Math.max(0, width - statusLen - 6)) +
127
- box.topRight;
160
+ // Top line: ╭─ status ─────────────────────────────────────────╮
161
+ const paddingLen = Math.max(0, width - statusLen - 6);
162
+ console.log(
163
+ theme.dim(box.topLeft + box.horizontal + ' ') +
164
+ statusText +
165
+ theme.dim(' ' + box.horizontal.repeat(paddingLen) + box.topRight)
166
+ );
167
+ }
128
168
 
129
- console.log(theme.dim(topLine.slice(0, 1)) + statusText + theme.dim(topLine.slice(statusLen + 4)));
169
+ /**
170
+ * Draw bottom of input box after user presses enter
171
+ */
172
+ function drawInputBoxBottom(): void {
173
+ const width = Math.min(getTerminalWidth(), 100);
174
+ console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
130
175
  }
131
176
 
132
177
  /**
@@ -139,10 +184,112 @@ function redrawUI(_state: SessionState): void {
139
184
  }
140
185
 
141
186
  /**
142
- * Prompt for input with styled prompt
187
+ * Prompt for input with styled prompt (inside box)
143
188
  */
144
189
  function getPrompt(): string {
145
- return theme.dim(box.vertical) + ' ' + theme.primary('') + ' ';
190
+ return theme.dim(box.vertical + ' ') + theme.primary('popeye') + theme.dim(' > ');
191
+ }
192
+
193
+ /**
194
+ * Prompt user to select an option
195
+ * Uses terminal: false to prevent echo issues when nested with main readline
196
+ */
197
+ async function promptSelection(
198
+ question: string,
199
+ options: { label: string; value: string }[],
200
+ defaultValue: string
201
+ ): Promise<string> {
202
+ return new Promise((resolve) => {
203
+ const rl = readline.createInterface({
204
+ input: process.stdin,
205
+ output: process.stdout,
206
+ terminal: false, // Prevent terminal mode to avoid echo issues
207
+ });
208
+
209
+ console.log();
210
+ console.log(theme.primary(` ${question}`));
211
+ options.forEach((opt, i) => {
212
+ const isDefault = opt.value === defaultValue;
213
+ console.log(` ${theme.dim(`${i + 1}.`)} ${opt.label}${isDefault ? theme.dim(' (default)') : ''}`);
214
+ });
215
+ console.log();
216
+
217
+ // Print prompt manually since terminal: false disables it
218
+ process.stdout.write(` Enter choice [1-${options.length}] or press Enter for default: `);
219
+
220
+ rl.once('line', (answer) => {
221
+ rl.close();
222
+ const trimmed = answer.trim();
223
+ if (!trimmed) {
224
+ resolve(defaultValue);
225
+ return;
226
+ }
227
+ const num = parseInt(trimmed, 10);
228
+ if (num >= 1 && num <= options.length) {
229
+ resolve(options[num - 1].value);
230
+ } else {
231
+ resolve(defaultValue);
232
+ }
233
+ });
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Prompt yes/no question
239
+ * Uses terminal: false to prevent echo issues when nested with main readline
240
+ */
241
+ async function promptYesNo(question: string, defaultYes: boolean = true): Promise<boolean> {
242
+ return new Promise((resolve) => {
243
+ const rl = readline.createInterface({
244
+ input: process.stdin,
245
+ output: process.stdout,
246
+ terminal: false, // Prevent terminal mode to avoid echo issues
247
+ });
248
+
249
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
250
+ process.stdout.write(` ${question} ${theme.dim(hint)} `);
251
+
252
+ rl.once('line', (answer) => {
253
+ rl.close();
254
+ const trimmed = answer.trim().toLowerCase();
255
+ if (!trimmed) {
256
+ resolve(defaultYes);
257
+ } else {
258
+ resolve(trimmed === 'y' || trimmed === 'yes');
259
+ }
260
+ });
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Check if reviewer/arbitrator has been configured (saved to config)
266
+ */
267
+ async function isConsensusConfigured(): Promise<boolean> {
268
+ const config = await loadConfig();
269
+ // Consider configured if enable_arbitration is true or if arbitrator is explicitly set to a provider
270
+ // (Default is arbitrator='off' and enable_arbitration=false, so any change indicates user configured it)
271
+ return config.consensus.enable_arbitration || config.consensus.arbitrator !== 'off';
272
+ }
273
+
274
+ /**
275
+ * Save reviewer/arbitrator settings to config file
276
+ */
277
+ async function saveConsensusConfig(state: SessionState): Promise<void> {
278
+ try {
279
+ // Load existing config and merge with new consensus settings
280
+ const existingConfig = await loadConfig();
281
+ const updatedConsensus = {
282
+ ...existingConfig.consensus,
283
+ reviewer: state.reviewer,
284
+ arbitrator: state.enableArbitration ? state.arbitrator : 'off' as const,
285
+ enable_arbitration: state.enableArbitration,
286
+ };
287
+ await saveConfig({
288
+ consensus: updatedConsensus,
289
+ }, true); // Save to global config
290
+ } catch (err) {
291
+ printWarning(`Could not save config: ${err instanceof Error ? err.message : 'Unknown error'}`);
292
+ }
146
293
  }
147
294
 
148
295
  /**
@@ -152,83 +299,181 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
152
299
  const status = await getAuthStatusForDisplay();
153
300
  state.claudeAuth = status.claude.authenticated;
154
301
  state.openaiAuth = status.openai.authenticated;
155
-
156
- if (state.claudeAuth && state.openaiAuth) {
157
- return true;
158
- }
302
+ state.geminiAuth = status.gemini?.authenticated || false;
159
303
 
160
304
  console.log();
161
- printWarning('Authentication required to continue');
305
+ printInfo('Checking authentication...');
162
306
  console.log();
163
307
 
164
308
  // Authenticate Claude if needed
165
309
  if (!state.claudeAuth) {
166
- console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude CLI') + theme.dim(' - Browser authentication'));
310
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude Code CLI') + theme.dim(' - Required for code generation'));
167
311
  console.log(theme.dim(box.vertical));
168
312
 
169
- const rl = readline.createInterface({
170
- input: process.stdin,
171
- output: process.stdout,
172
- });
313
+ try {
314
+ const success = await authenticateClaude();
315
+ if (success) {
316
+ printSuccess('Claude Code CLI ready');
317
+ state.claudeAuth = true;
318
+ } else {
319
+ printWarning('Claude Code CLI not authenticated - run "claude login" to authenticate');
320
+ }
321
+ } catch (err) {
322
+ printError(err instanceof Error ? err.message : 'Authentication failed');
323
+ }
324
+ console.log();
325
+ } else {
326
+ printSuccess('Claude Code CLI ready');
327
+ }
173
328
 
174
- const proceed = await new Promise<boolean>((resolve) => {
175
- rl.question(theme.dim(box.vertical) + ' Press Enter to open browser (or "skip" to skip): ', (answer) => {
176
- rl.close();
177
- resolve(answer.toLowerCase() !== 'skip');
178
- });
179
- });
329
+ // Authenticate OpenAI if needed
330
+ if (!state.openaiAuth) {
331
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - Required for consensus review'));
332
+ console.log(theme.dim(box.vertical));
333
+
334
+ try {
335
+ const success = await authenticateOpenAI();
336
+ if (success) {
337
+ printSuccess('OpenAI API ready');
338
+ state.openaiAuth = true;
339
+ } else {
340
+ printWarning('OpenAI API not authenticated');
341
+ }
342
+ } catch (err) {
343
+ printError(err instanceof Error ? err.message : 'Authentication failed');
344
+ }
345
+ console.log();
346
+ } else {
347
+ printSuccess('OpenAI API ready');
348
+ }
349
+
350
+ // Check if reviewer/arbitrator is already configured
351
+ const alreadyConfigured = await isConsensusConfigured();
352
+
353
+ // Only ask about reviewer/arbitrator if not already configured
354
+ if (state.claudeAuth && state.openaiAuth && !alreadyConfigured) {
355
+ console.log();
356
+ console.log(theme.primary.bold(' AI Configuration'));
357
+ console.log(theme.dim(' Claude generates code. Choose who reviews and arbitrates:'));
358
+
359
+ // Ask who should review plans
360
+ state.reviewer = await promptSelection(
361
+ 'Who should review Claude\'s plans?',
362
+ [
363
+ { label: theme.secondary('OpenAI') + theme.dim(' - GPT-4o reviews plans'), value: 'openai' },
364
+ { label: theme.secondary('Gemini') + theme.dim(' - Gemini 2.0 reviews plans'), value: 'gemini' },
365
+ ],
366
+ 'openai'
367
+ ) as AIProvider;
368
+
369
+ // Ask about arbitration
370
+ console.log();
371
+ state.enableArbitration = await promptYesNo(
372
+ theme.primary('Enable arbitration when consensus is stuck?'),
373
+ true
374
+ );
375
+
376
+ if (state.enableArbitration) {
377
+ // Auto-select the other provider as arbitrator
378
+ const defaultArbitrator = state.reviewer === 'openai' ? 'gemini' : 'openai';
379
+
380
+ state.arbitrator = await promptSelection(
381
+ 'Who should arbitrate when stuck?',
382
+ [
383
+ { label: theme.secondary('Gemini') + theme.dim(' - Google Gemini breaks deadlocks'), value: 'gemini' },
384
+ { label: theme.secondary('OpenAI') + theme.dim(' - OpenAI breaks deadlocks'), value: 'openai' },
385
+ ],
386
+ defaultArbitrator
387
+ ) as AIProvider;
388
+
389
+ // Authenticate Gemini if needed for reviewer or arbitrator
390
+ const needsGemini = state.reviewer === 'gemini' || state.arbitrator === 'gemini';
391
+ if (needsGemini && !state.geminiAuth) {
392
+ console.log();
393
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
394
+ console.log(theme.dim(box.vertical));
395
+
396
+ try {
397
+ const success = await authenticateGemini();
398
+ if (success) {
399
+ printSuccess('Gemini API ready');
400
+ state.geminiAuth = true;
401
+ } else {
402
+ printWarning('Gemini API not authenticated - arbitration disabled');
403
+ state.enableArbitration = false;
404
+ }
405
+ } catch (err) {
406
+ printError(err instanceof Error ? err.message : 'Authentication failed');
407
+ state.enableArbitration = false;
408
+ }
409
+ }
410
+ }
411
+
412
+ // Also check if reviewer is gemini and we need to auth
413
+ if (state.reviewer === 'gemini' && !state.geminiAuth) {
414
+ console.log();
415
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for review'));
416
+ console.log(theme.dim(box.vertical));
180
417
 
181
- if (proceed) {
182
- startSpinner('Opening browser for Claude authentication...');
183
418
  try {
184
- const success = await authenticateClaude();
419
+ const success = await authenticateGemini();
185
420
  if (success) {
186
- succeedSpinner('Claude authenticated');
187
- state.claudeAuth = true;
421
+ printSuccess('Gemini API ready');
422
+ state.geminiAuth = true;
188
423
  } else {
189
- failSpinner('Claude authentication failed');
424
+ printWarning('Gemini API not authenticated - falling back to OpenAI');
425
+ state.reviewer = 'openai';
190
426
  }
191
427
  } catch (err) {
192
- failSpinner('Claude authentication failed');
193
428
  printError(err instanceof Error ? err.message : 'Authentication failed');
429
+ state.reviewer = 'openai';
194
430
  }
195
431
  }
196
- console.log();
197
- }
198
432
 
199
- // Authenticate OpenAI if needed
200
- if (!state.openaiAuth) {
201
- console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - API key required'));
202
- console.log(theme.dim(box.vertical));
433
+ // Save the configuration to persist between sessions
434
+ await saveConsensusConfig(state);
203
435
 
204
- const rl2 = readline.createInterface({
205
- input: process.stdin,
206
- output: process.stdout,
207
- });
436
+ // Show summary
437
+ console.log();
438
+ console.log(theme.secondary(' Configuration saved. Use /config to change later.'));
439
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
440
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
441
+ console.log();
442
+ } else if (state.claudeAuth && state.openaiAuth && alreadyConfigured) {
443
+ // Show loaded configuration
444
+ console.log();
445
+ console.log(theme.secondary(' Using saved configuration (use /config to change):'));
446
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
447
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
448
+ console.log();
208
449
 
209
- const proceed2 = await new Promise<boolean>((resolve) => {
210
- rl2.question(theme.dim(box.vertical) + ' Press Enter to open key entry page (or "skip" to skip): ', (answer) => {
211
- rl2.close();
212
- resolve(answer.toLowerCase() !== 'skip');
213
- });
214
- });
450
+ // Authenticate Gemini if needed based on saved config
451
+ const needsGemini = state.reviewer === 'gemini' || (state.enableArbitration && state.arbitrator === 'gemini');
452
+ if (needsGemini && !state.geminiAuth) {
453
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
454
+ console.log(theme.dim(box.vertical));
215
455
 
216
- if (proceed2) {
217
- startSpinner('Opening browser for OpenAI API key entry...');
218
456
  try {
219
- const success = await authenticateOpenAI();
457
+ const success = await authenticateGemini();
220
458
  if (success) {
221
- succeedSpinner('OpenAI authenticated');
222
- state.openaiAuth = true;
459
+ printSuccess('Gemini API ready');
460
+ state.geminiAuth = true;
223
461
  } else {
224
- failSpinner('OpenAI authentication failed');
462
+ printWarning('Gemini API not authenticated');
463
+ if (state.reviewer === 'gemini') {
464
+ printWarning('Falling back to OpenAI as reviewer');
465
+ state.reviewer = 'openai';
466
+ }
467
+ if (state.enableArbitration && state.arbitrator === 'gemini') {
468
+ printWarning('Disabling arbitration');
469
+ state.enableArbitration = false;
470
+ }
225
471
  }
226
472
  } catch (err) {
227
- failSpinner('OpenAI authentication failed');
228
- printError(err instanceof Error ? err.message : 'Authentication failed');
473
+ printError(err instanceof Error ? err.message : 'Gemini authentication failed');
229
474
  }
475
+ console.log();
230
476
  }
231
- console.log();
232
477
  }
233
478
 
234
479
  return state.claudeAuth && state.openaiAuth;
@@ -244,11 +489,14 @@ function showHelp(): void {
244
489
 
245
490
  const commands = [
246
491
  ['/help', 'Show this help message'],
492
+ ['/info', 'Show system info (Claude CLI status, etc.)'],
247
493
  ['/status', 'Show current project status'],
248
494
  ['/auth', 'Re-authenticate services'],
249
- ['/config', 'Show configuration'],
250
- ['/language <lang>', 'Set language (python/typescript)'],
251
- ['/model <model>', 'Set OpenAI model'],
495
+ ['/config', 'Show/change configuration'],
496
+ ['/config reviewer', 'Set reviewer (openai/gemini)'],
497
+ ['/config arbitrator', 'Set arbitrator (openai/gemini/off)'],
498
+ ['/lang <lang>', 'Set language (python/typescript)'],
499
+ ['/new <idea>', 'Force start a new project (skips existing check)'],
252
500
  ['/resume', 'Resume interrupted project'],
253
501
  ['/clear', 'Clear screen'],
254
502
  ['/exit', 'Exit Popeye'],
@@ -259,7 +507,65 @@ function showHelp(): void {
259
507
  }
260
508
 
261
509
  console.log();
262
- console.log(theme.secondary(' Or just type your project idea to get started!'));
510
+ console.log(theme.secondary(' Type your project idea to get started!'));
511
+ console.log(theme.secondary(' Example: "A REST API for managing todo items"'));
512
+ console.log();
513
+ }
514
+
515
+ /**
516
+ * Handle /info command - show system info
517
+ */
518
+ async function handleInfo(): Promise<void> {
519
+ console.log();
520
+ console.log(theme.primary.bold(' System Info:'));
521
+ console.log();
522
+
523
+ // Check Claude CLI
524
+ const claudeInstalled = await isClaudeCLIInstalled();
525
+ const claudeStatus = await checkClaudeCLIAuth();
526
+
527
+ console.log(theme.secondary(' Claude Code:'));
528
+ console.log(` ${theme.dim('Installed:')} ${claudeInstalled ? theme.success('Yes') : theme.error('No')}`);
529
+
530
+ if (claudeInstalled) {
531
+ console.log(` ${theme.dim('Authenticated:')} ${claudeStatus.authenticated ? theme.success('Yes') : theme.warning('No')}`);
532
+ console.log(` ${theme.dim('Model:')} ${theme.primary('Uses your Claude Code settings')}`);
533
+ console.log(` ${theme.dim('MCPs:')} ${theme.primary('Uses your configured MCP servers')}`);
534
+
535
+ if (!claudeStatus.authenticated) {
536
+ console.log();
537
+ console.log(` ${theme.warning('Run:')} ${theme.primary('claude login')} ${theme.warning('to authenticate')}`);
538
+ }
539
+ } else {
540
+ console.log();
541
+ console.log(` ${theme.warning('Install:')} ${theme.primary('npm install -g @anthropic-ai/claude-code')}`);
542
+ }
543
+
544
+ console.log();
545
+ console.log(theme.secondary(' OpenAI:'));
546
+ const authStatus = await getAuthStatusForDisplay();
547
+ console.log(` ${theme.dim('Authenticated:')} ${authStatus.openai.authenticated ? theme.success('Yes') : theme.warning('No')}`);
548
+ if (authStatus.openai.authenticated && authStatus.openai.keyLastFour) {
549
+ console.log(` ${theme.dim('API Key:')} ${theme.dim(authStatus.openai.keyLastFour)}`);
550
+ }
551
+
552
+ console.log();
553
+ console.log(theme.secondary(' Gemini:'));
554
+ const geminiStatus = await checkGeminiAuth();
555
+ console.log(` ${theme.dim('Authenticated:')} ${geminiStatus.authenticated ? theme.success('Yes') : theme.dim('No')}`);
556
+ if (geminiStatus.authenticated && geminiStatus.keyLastFour) {
557
+ console.log(` ${theme.dim('API Key:')} ${theme.dim(geminiStatus.keyLastFour)}`);
558
+ }
559
+
560
+ console.log();
561
+ console.log(theme.secondary(' Environment:'));
562
+ console.log(` ${theme.dim('Node.js:')} ${process.version}`);
563
+ console.log(` ${theme.dim('Platform:')} ${process.platform}`);
564
+ console.log(` ${theme.dim('Working Dir:')} ${process.cwd()}`);
565
+ console.log();
566
+
567
+ console.log(theme.dim(' Tip: Claude Code model and MCP settings are configured in your'));
568
+ console.log(theme.dim(' Claude Code CLI. Run "claude config" to see/change them.'));
263
569
  console.log();
264
570
  }
265
571
 
@@ -271,6 +577,13 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
271
577
 
272
578
  if (!trimmed) return true;
273
579
 
580
+ // Check for common words that should be commands (without /)
581
+ const lowerTrimmed = trimmed.toLowerCase();
582
+ if (['help', 'exit', 'quit', 'info', 'status', 'config'].includes(lowerTrimmed)) {
583
+ printWarning(`Did you mean /${lowerTrimmed}? Use / prefix for commands.`);
584
+ return true;
585
+ }
586
+
274
587
  // Handle commands
275
588
  if (trimmed.startsWith('/')) {
276
589
  const [cmd, ...args] = trimmed.split(/\s+/);
@@ -278,9 +591,16 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
278
591
 
279
592
  switch (command) {
280
593
  case '/help':
594
+ case '/h':
595
+ case '/?':
281
596
  showHelp();
282
597
  break;
283
598
 
599
+ case '/info':
600
+ case '/check':
601
+ await handleInfo();
602
+ break;
603
+
284
604
  case '/exit':
285
605
  case '/quit':
286
606
  case '/q':
@@ -289,6 +609,7 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
289
609
  return false;
290
610
 
291
611
  case '/clear':
612
+ case '/cls':
292
613
  redrawUI(state);
293
614
  break;
294
615
 
@@ -301,19 +622,32 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
301
622
  break;
302
623
 
303
624
  case '/config':
304
- await handleConfig(state);
625
+ await handleConfig(state, args);
305
626
  break;
306
627
 
307
628
  case '/language':
629
+ case '/lang':
630
+ case '/l':
308
631
  handleLanguage(args, state);
309
632
  break;
310
633
 
311
634
  case '/model':
635
+ case '/m':
312
636
  handleModel(args, state);
313
637
  break;
314
638
 
315
639
  case '/resume':
316
- await handleResume(state);
640
+ await handleResume(state, args);
641
+ break;
642
+
643
+ case '/new':
644
+ // Force start a new project even if existing projects found
645
+ if (args.length === 0) {
646
+ printError('Usage: /new <project idea>');
647
+ printInfo('Example: /new todo app with user authentication');
648
+ } else {
649
+ await handleNewProject(args.join(' '), state);
650
+ }
317
651
  break;
318
652
 
319
653
  default:
@@ -324,6 +658,13 @@ async function handleInput(input: string, state: SessionState): Promise<boolean>
324
658
  return true;
325
659
  }
326
660
 
661
+ // Warn if input is too short (likely accidental)
662
+ if (trimmed.length < 10) {
663
+ printWarning(`Input "${trimmed}" is very short. Did you mean to type a command?`);
664
+ printInfo('Type /help for commands, or enter a longer project description.');
665
+ return true;
666
+ }
667
+
327
668
  // Handle as project idea
328
669
  await handleIdea(trimmed, state);
329
670
  return true;
@@ -355,20 +696,107 @@ async function handleStatus(state: SessionState): Promise<void> {
355
696
  /**
356
697
  * Handle /config command
357
698
  */
358
- async function handleConfig(state: SessionState): Promise<void> {
699
+ async function handleConfig(state: SessionState, args: string[] = []): Promise<void> {
359
700
  const config = await loadConfig();
360
701
 
702
+ // Handle config subcommands
703
+ if (args.length > 0) {
704
+ const subcommand = args[0].toLowerCase();
705
+
706
+ switch (subcommand) {
707
+ case 'reviewer':
708
+ if (args.length > 1) {
709
+ const newReviewer = args[1].toLowerCase();
710
+ if (newReviewer === 'openai' || newReviewer === 'gemini') {
711
+ if (newReviewer === 'gemini' && !state.geminiAuth) {
712
+ printWarning('Gemini API not authenticated. Run /auth first.');
713
+ return;
714
+ }
715
+ state.reviewer = newReviewer as AIProvider;
716
+ // Save to config
717
+ await saveConsensusConfig(state);
718
+ printSuccess(`Reviewer set to ${newReviewer}`);
719
+ } else {
720
+ printError('Invalid reviewer. Use: openai or gemini');
721
+ }
722
+ } else {
723
+ printKeyValue('Reviewer', state.reviewer);
724
+ printInfo('Use: /config reviewer <openai|gemini>');
725
+ }
726
+ return;
727
+
728
+ case 'arbitrator':
729
+ if (args.length > 1) {
730
+ const newArbitrator = args[1].toLowerCase();
731
+ if (newArbitrator === 'openai' || newArbitrator === 'gemini') {
732
+ if (newArbitrator === 'gemini' && !state.geminiAuth) {
733
+ printWarning('Gemini API not authenticated. Run /auth first.');
734
+ return;
735
+ }
736
+ state.arbitrator = newArbitrator as AIProvider;
737
+ state.enableArbitration = true;
738
+ // Save to config
739
+ await saveConsensusConfig(state);
740
+ printSuccess(`Arbitrator set to ${newArbitrator}`);
741
+ } else if (newArbitrator === 'off' || newArbitrator === 'none') {
742
+ state.enableArbitration = false;
743
+ // Save to config
744
+ await saveConsensusConfig(state);
745
+ printSuccess('Arbitration disabled');
746
+ } else {
747
+ printError('Invalid arbitrator. Use: openai, gemini, or off');
748
+ }
749
+ } else {
750
+ printKeyValue('Arbitrator', state.enableArbitration ? state.arbitrator : 'disabled');
751
+ printInfo('Use: /config arbitrator <openai|gemini|off>');
752
+ }
753
+ return;
754
+
755
+ case 'language':
756
+ case 'lang':
757
+ if (args.length > 1) {
758
+ const lang = args[1].toLowerCase() as OutputLanguage;
759
+ if (['python', 'typescript'].includes(lang)) {
760
+ state.language = lang;
761
+ printSuccess(`Language set to ${lang}`);
762
+ } else {
763
+ printError('Invalid language. Use: python or typescript');
764
+ }
765
+ } else {
766
+ printKeyValue('Language', state.language);
767
+ }
768
+ return;
769
+
770
+ default:
771
+ printError(`Unknown config option: ${subcommand}`);
772
+ printInfo('Options: reviewer, arbitrator, language');
773
+ return;
774
+ }
775
+ }
776
+
777
+ // Show full config
361
778
  console.log();
362
779
  console.log(theme.primary.bold(' Session:'));
363
- console.log(` ${theme.dim('Directory:')} ${state.projectDir || 'Not set'}`);
364
- console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
365
- console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
366
- console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('●') : theme.error('○')}`);
367
- console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('●') : theme.error('○')}`);
780
+ console.log(` ${theme.dim('Directory:')} ${state.projectDir || 'Not set'}`);
781
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
782
+ console.log();
783
+ console.log(theme.primary.bold(' Authentication:'));
784
+ console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
785
+ console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
786
+ console.log(` ${theme.dim('Gemini:')} ${state.geminiAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
787
+ console.log();
788
+ console.log(theme.primary.bold(' AI Configuration:'));
789
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
790
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
368
791
  console.log();
369
792
  console.log(theme.primary.bold(' Consensus:'));
370
- console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
371
- console.log(` ${theme.dim('Max Iterations:')} ${config.consensus.max_disagreements}`);
793
+ console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
794
+ console.log(` ${theme.dim('Max Iters:')} ${config.consensus.max_disagreements}`);
795
+ console.log();
796
+ console.log(theme.secondary(' Change settings:'));
797
+ console.log(theme.dim(' /config reviewer <openai|gemini>'));
798
+ console.log(theme.dim(' /config arbitrator <openai|gemini|off>'));
799
+ console.log(theme.dim(' /config language <python|typescript>'));
372
800
  console.log();
373
801
  }
374
802
 
@@ -418,32 +846,578 @@ function handleModel(args: string[], state: SessionState): void {
418
846
  printSuccess(`Model set to ${model}`);
419
847
  }
420
848
 
849
+ /**
850
+ * Prompt for additional context
851
+ * Uses terminal: false to prevent echo issues when nested with main readline
852
+ */
853
+ async function promptForContext(prompt: string): Promise<string> {
854
+ return new Promise((resolve) => {
855
+ const rl = readline.createInterface({
856
+ input: process.stdin,
857
+ output: process.stdout,
858
+ terminal: false, // Prevent terminal mode to avoid echo issues
859
+ });
860
+
861
+ console.log();
862
+ console.log(theme.primary(` ${prompt}`));
863
+ console.log(theme.dim(' (Press Enter to skip, or type your guidance)'));
864
+ console.log();
865
+
866
+ process.stdout.write(' > ');
867
+
868
+ rl.once('line', (answer) => {
869
+ rl.close();
870
+ resolve(answer.trim());
871
+ });
872
+ });
873
+ }
874
+
875
+ /**
876
+ * Discovered project context from docs/ folder
877
+ */
878
+ interface DiscoveredProject {
879
+ found: boolean;
880
+ name?: string;
881
+ idea?: string;
882
+ plan?: string;
883
+ planFile?: string;
884
+ readme?: string;
885
+ language?: OutputLanguage;
886
+ hasCode: boolean;
887
+ codeFiles: string[];
888
+ }
889
+
890
+ /**
891
+ * Discover project context from docs/ and codebase
892
+ */
893
+ async function discoverProjectContext(projectDir: string): Promise<DiscoveredProject> {
894
+ const result: DiscoveredProject = {
895
+ found: false,
896
+ hasCode: false,
897
+ codeFiles: [],
898
+ };
899
+
900
+ const docsDir = path.join(projectDir, 'docs');
901
+
902
+ // Try to read plan files
903
+ const planFiles = ['PLAN.md', 'PLAN-DRAFT.md'];
904
+ for (const planFile of planFiles) {
905
+ try {
906
+ const planPath = path.join(docsDir, planFile);
907
+ const content = await fs.readFile(planPath, 'utf-8');
908
+ result.plan = content;
909
+ result.planFile = planFile;
910
+ result.found = true;
911
+
912
+ // Try to extract project name from plan
913
+ const nameMatch = content.match(/^#\s*(?:Development Plan|Project):\s*(.+)$/mi) ||
914
+ content.match(/^#\s*(.+)$/m);
915
+ if (nameMatch) {
916
+ result.name = nameMatch[1].replace(/Development Plan/i, '').trim();
917
+ }
918
+
919
+ // Try to extract idea/overview from plan
920
+ const overviewMatch = content.match(/(?:Overview|Summary|Description)[:\s]*\n+([\s\S]*?)(?=\n#|\n\*\*|$)/i);
921
+ if (overviewMatch) {
922
+ result.idea = overviewMatch[1].trim().slice(0, 500);
923
+ }
924
+ break;
925
+ } catch {
926
+ // File doesn't exist, continue
927
+ }
928
+ }
929
+
930
+ // Try to read README
931
+ try {
932
+ const readmePath = path.join(projectDir, 'README.md');
933
+ const content = await fs.readFile(readmePath, 'utf-8');
934
+ result.readme = content;
935
+ result.found = true;
936
+
937
+ // Extract project name from README if not already found
938
+ if (!result.name) {
939
+ const nameMatch = content.match(/^#\s*(.+)$/m);
940
+ if (nameMatch) {
941
+ result.name = nameMatch[1].trim();
942
+ }
943
+ }
944
+
945
+ // Extract idea from README if not found in plan
946
+ if (!result.idea) {
947
+ const lines = content.split('\n').slice(1, 10).join('\n').trim();
948
+ if (lines.length > 20) {
949
+ result.idea = lines.slice(0, 500);
950
+ }
951
+ }
952
+ } catch {
953
+ // README doesn't exist
954
+ }
955
+
956
+ // Scan for code files to detect language
957
+ try {
958
+ const files = await fs.readdir(projectDir, { recursive: true });
959
+ const codeExtensions = {
960
+ python: ['.py'],
961
+ typescript: ['.ts', '.tsx', '.js', '.jsx'],
962
+ };
963
+
964
+ let pyCount = 0;
965
+ let tsCount = 0;
966
+
967
+ for (const file of files) {
968
+ const fileName = String(file);
969
+ if (fileName.includes('node_modules') || fileName.includes('.git') || fileName.includes('__pycache__')) {
970
+ continue;
971
+ }
972
+
973
+ if (codeExtensions.python.some(ext => fileName.endsWith(ext))) {
974
+ pyCount++;
975
+ result.codeFiles.push(fileName);
976
+ }
977
+ if (codeExtensions.typescript.some(ext => fileName.endsWith(ext))) {
978
+ tsCount++;
979
+ result.codeFiles.push(fileName);
980
+ }
981
+ }
982
+
983
+ result.hasCode = result.codeFiles.length > 0;
984
+
985
+ // Determine language from code files
986
+ if (pyCount > tsCount) {
987
+ result.language = 'python';
988
+ } else if (tsCount > 0) {
989
+ result.language = 'typescript';
990
+ }
991
+ } catch {
992
+ // Can't read directory
993
+ }
994
+
995
+ return result;
996
+ }
997
+
421
998
  /**
422
999
  * Handle /resume command
423
1000
  */
424
- async function handleResume(state: SessionState): Promise<void> {
1001
+ async function handleResume(state: SessionState, args: string[]): Promise<void> {
1002
+ if (!state.claudeAuth || !state.openaiAuth) {
1003
+ printError('Authentication required. Run /auth first.');
1004
+ return;
1005
+ }
1006
+
1007
+ // Discover all projects (registered + scanned in current directory)
1008
+ console.log();
1009
+ printInfo('Scanning for projects...');
1010
+
1011
+ const { all: allProjects } = await discoverProjects(state.projectDir || process.cwd());
1012
+
1013
+ // If projects found, let user select one
1014
+ if (allProjects.length > 0) {
1015
+ console.log();
1016
+ console.log(theme.primary.bold(' Found Projects:'));
1017
+ console.log();
1018
+
1019
+ // Show project list with numbers
1020
+ const displayProjects = allProjects.slice(0, 10); // Limit to 10
1021
+ for (let i = 0; i < displayProjects.length; i++) {
1022
+ const project = displayProjects[i];
1023
+ const info = formatProjectForDisplay(project);
1024
+ const statusColor = project.status === 'complete' ? theme.success :
1025
+ project.status === 'failed' ? theme.error :
1026
+ project.status === 'in-progress' ? theme.warning : theme.dim;
1027
+
1028
+ console.log(` ${theme.primary(`${i + 1}.`)} ${theme.secondary(info.name)}`);
1029
+ console.log(` ${statusColor(info.status)} ${theme.dim('|')} ${info.age}`);
1030
+ console.log(` ${theme.dim(info.path)}`);
1031
+ if (project.idea) {
1032
+ console.log(` ${theme.dim(project.idea.slice(0, 60))}${project.idea.length > 60 ? '...' : ''}`);
1033
+ }
1034
+ console.log();
1035
+ }
1036
+
1037
+ if (allProjects.length > 10) {
1038
+ console.log(theme.dim(` ... and ${allProjects.length - 10} more projects`));
1039
+ console.log();
1040
+ }
1041
+
1042
+ // Let user select
1043
+ const selection = await promptSelection(
1044
+ 'Select a project to resume:',
1045
+ [
1046
+ ...displayProjects.map((p, i) => ({
1047
+ value: String(i),
1048
+ label: `${p.name} (${formatProjectForDisplay(p).age})`,
1049
+ })),
1050
+ { value: 'scan', label: 'Scan for more projects...' },
1051
+ { value: 'cancel', label: 'Cancel' },
1052
+ ],
1053
+ '0'
1054
+ );
1055
+
1056
+ if (selection === 'cancel') {
1057
+ printInfo('Cancelled');
1058
+ return;
1059
+ }
1060
+
1061
+ if (selection === 'scan') {
1062
+ // Scan deeper in current directory
1063
+ printInfo('Scanning subdirectories...');
1064
+ const { all: deepScan } = await discoverProjects(state.projectDir || process.cwd());
1065
+ if (deepScan.length === allProjects.length) {
1066
+ printWarning('No additional projects found');
1067
+ } else {
1068
+ printSuccess(`Found ${deepScan.length - allProjects.length} additional projects`);
1069
+ }
1070
+ // Recursively call handleResume to show updated list
1071
+ await handleResume(state, args);
1072
+ return;
1073
+ }
1074
+
1075
+ const selectedIndex = parseInt(selection, 10);
1076
+ const selectedProject = displayProjects[selectedIndex];
1077
+
1078
+ if (!selectedProject) {
1079
+ printError('Invalid selection');
1080
+ return;
1081
+ }
1082
+
1083
+ // Set the project directory and continue
1084
+ state.projectDir = selectedProject.path;
1085
+ console.log();
1086
+ printInfo(`Selected: ${selectedProject.name}`);
1087
+ }
1088
+
1089
+ // Now check for formal project state at the selected/current directory
425
1090
  if (!state.projectDir) {
426
1091
  printError('No project directory set');
427
1092
  return;
428
1093
  }
429
1094
 
430
- if (!state.claudeAuth || !state.openaiAuth) {
431
- printError('Authentication required. Run /auth first.');
1095
+ const status = await getWorkflowStatus(state.projectDir);
1096
+
1097
+ if (status.exists && status.state) {
1098
+ // Formal project state exists - analyze actual progress before resuming
1099
+ // Update session state to reflect project's language (preserves language on resume)
1100
+ state.language = status.state.language;
1101
+
1102
+ // Get detailed progress analysis
1103
+ const progressAnalysis = await analyzeProjectProgress(state.projectDir);
1104
+ const verification = await verifyProjectCompletion(state.projectDir);
1105
+
1106
+ console.log();
1107
+ console.log(theme.primary.bold(' Project Status:'));
1108
+ console.log(` ${theme.dim('Name:')} ${status.state.name}`);
1109
+ console.log(` ${theme.dim('Language:')} ${theme.primary(status.state.language)}`);
1110
+ console.log(` ${theme.dim('Phase:')} ${theme.primary(status.state.phase)}`);
1111
+ console.log(` ${theme.dim('Status:')} ${status.state.status}`);
1112
+
1113
+ // Show detailed progress comparison
1114
+ console.log();
1115
+ console.log(theme.primary.bold(' Progress Analysis:'));
1116
+ console.log(` ${theme.dim('Milestones:')} ${progressAnalysis.completedMilestones}/${progressAnalysis.totalMilestones} complete`);
1117
+ console.log(` ${theme.dim('Tasks:')} ${progressAnalysis.completedTasks}/${progressAnalysis.totalTasks} complete (${progressAnalysis.percentComplete}%)`);
1118
+
1119
+ if (progressAnalysis.inProgressTasks > 0) {
1120
+ console.log(` ${theme.dim('In Progress:')} ${theme.warning(String(progressAnalysis.inProgressTasks))} task(s)`);
1121
+ }
1122
+ if (progressAnalysis.failedTasks > 0) {
1123
+ console.log(` ${theme.dim('Failed:')} ${theme.error(String(progressAnalysis.failedTasks))} task(s)`);
1124
+ }
1125
+ if (progressAnalysis.pendingTasks > 0) {
1126
+ console.log(` ${theme.dim('Pending:')} ${progressAnalysis.pendingTasks} task(s)`);
1127
+ }
1128
+
1129
+ // Show plan file comparison
1130
+ if (progressAnalysis.planTaskCount > 0) {
1131
+ console.log();
1132
+ console.log(theme.primary.bold(' Plan Comparison (from docs/PLAN.md):'));
1133
+ console.log(` ${theme.dim('Plan Tasks:')} ${progressAnalysis.planTaskCount} tasks found in plan`);
1134
+ console.log(` ${theme.dim('State Tasks:')} ${progressAnalysis.totalTasks} tasks in state`);
1135
+
1136
+ // Show plan mismatch warning (critical - plan has more tasks than state)
1137
+ if (progressAnalysis.planMismatch) {
1138
+ console.log();
1139
+ console.log(theme.error.bold(' CRITICAL: Plan Mismatch Detected!'));
1140
+ console.log(theme.error(` The plan file has ${progressAnalysis.planTaskCount} tasks but state only has ${progressAnalysis.totalTasks}.`));
1141
+ console.log(theme.error(` This means the plan was not fully parsed into tasks.`));
1142
+ console.log(theme.error(` True progress: ${progressAnalysis.completedTasks}/${progressAnalysis.planTaskCount} tasks (${progressAnalysis.percentComplete}%)`));
1143
+
1144
+ // Show some missing tasks
1145
+ if (progressAnalysis.missingFromState.length > 0) {
1146
+ console.log();
1147
+ console.log(theme.warning(' Tasks in plan but missing from state:'));
1148
+ for (const task of progressAnalysis.missingFromState.slice(0, 8)) {
1149
+ console.log(` ${theme.dim('-')} ${task.slice(0, 70)}${task.length > 70 ? '...' : ''}`);
1150
+ }
1151
+ if (progressAnalysis.missingFromState.length > 8) {
1152
+ console.log(` ${theme.dim(`... and ${progressAnalysis.missingFromState.length - 8} more tasks`)}`);
1153
+ }
1154
+ }
1155
+
1156
+ console.log();
1157
+ console.log(theme.secondary(' The plan needs to be re-parsed to capture all tasks.'));
1158
+ console.log(theme.secondary(' Consider running the workflow again or manually adding tasks.'));
1159
+ }
1160
+ } else if (progressAnalysis.planParseError) {
1161
+ console.log();
1162
+ console.log(theme.dim(` Plan file: ${progressAnalysis.planParseError}`));
1163
+ }
1164
+
1165
+ // Check for status mismatch (status says complete but state tasks are incomplete)
1166
+ if (progressAnalysis.statusMismatch && !progressAnalysis.planMismatch) {
1167
+ console.log();
1168
+ console.log(theme.warning.bold(' WARNING: Status Mismatch Detected!'));
1169
+ console.log(theme.warning(` Project status says '${status.state.status}' but work is incomplete.`));
1170
+ console.log(theme.warning(` ${progressAnalysis.progressSummary}`));
1171
+ console.log(theme.secondary(' Will reset status and continue execution.'));
1172
+ }
1173
+
1174
+ // Show next items to work on
1175
+ if (progressAnalysis.nextMilestone || progressAnalysis.nextTask) {
1176
+ console.log();
1177
+ console.log(theme.secondary(' Next Up:'));
1178
+ if (progressAnalysis.nextMilestone) {
1179
+ console.log(` ${theme.dim('Milestone:')} ${progressAnalysis.nextMilestone.name}`);
1180
+ }
1181
+ if (progressAnalysis.nextTask) {
1182
+ console.log(` ${theme.dim('Task:')} ${progressAnalysis.nextTask.name}`);
1183
+ }
1184
+ }
1185
+
1186
+ // Show incomplete milestones
1187
+ if (progressAnalysis.incompleteMilestones.length > 0 && !verification.isComplete) {
1188
+ console.log();
1189
+ console.log(theme.secondary(' Remaining Milestones:'));
1190
+ for (const m of progressAnalysis.incompleteMilestones.slice(0, 5)) {
1191
+ console.log(` ${theme.dim('-')} ${m.name} (${m.tasksRemaining} tasks remaining)`);
1192
+ }
1193
+ if (progressAnalysis.incompleteMilestones.length > 5) {
1194
+ console.log(` ${theme.dim(`... and ${progressAnalysis.incompleteMilestones.length - 5} more`)}`);
1195
+ }
1196
+ }
1197
+
1198
+ if (status.state.consensusHistory && status.state.consensusHistory.length > 0) {
1199
+ const lastConsensus = status.state.consensusHistory[status.state.consensusHistory.length - 1];
1200
+ console.log();
1201
+ console.log(theme.secondary(' Consensus History:'));
1202
+ console.log(` ${theme.dim('Last Score:')} ${lastConsensus.result.score}%`);
1203
+ console.log(` ${theme.dim('Iterations:')} ${status.state.consensusHistory.length}`);
1204
+
1205
+ // Show last concerns
1206
+ if (lastConsensus.result.concerns && lastConsensus.result.concerns.length > 0) {
1207
+ console.log();
1208
+ console.log(theme.secondary(' Last Concerns:'));
1209
+ for (const concern of lastConsensus.result.concerns.slice(0, 3)) {
1210
+ console.log(` ${theme.dim('-')} ${concern.slice(0, 80)}${concern.length > 80 ? '...' : ''}`);
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ if (status.state.error) {
1216
+ console.log();
1217
+ console.log(theme.error(` Error: ${status.state.error}`));
1218
+ }
1219
+
1220
+ // If project says complete but isn't, inform user we'll continue
1221
+ if (verification.isComplete) {
1222
+ console.log();
1223
+ printSuccess('Project is fully complete!');
1224
+ printInfo(`All ${progressAnalysis.totalTasks} tasks across ${progressAnalysis.totalMilestones} milestones are done.`);
1225
+ return;
1226
+ }
1227
+
1228
+ // Check if user provided context as argument
1229
+ let additionalContext = args.join(' ').trim();
1230
+
1231
+ // If no context provided, ask if they want to add guidance
1232
+ if (!additionalContext) {
1233
+ console.log();
1234
+ const wantsContext = await promptYesNo(
1235
+ theme.primary('Would you like to add guidance before resuming?'),
1236
+ false
1237
+ );
1238
+
1239
+ if (wantsContext) {
1240
+ additionalContext = await promptForContext(
1241
+ 'What guidance would you like to give? (e.g., "Focus on simplicity", "Use SQLite instead of PostgreSQL")'
1242
+ );
1243
+ }
1244
+ }
1245
+
1246
+ console.log();
1247
+ printInfo('Resuming workflow...');
1248
+ if (additionalContext) {
1249
+ console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
1250
+ }
1251
+ console.log();
1252
+
1253
+ const result = await resumeWorkflow(state.projectDir, {
1254
+ consensusConfig: {
1255
+ reviewer: state.reviewer,
1256
+ arbitrator: state.arbitrator,
1257
+ enableArbitration: state.enableArbitration,
1258
+ },
1259
+ additionalContext,
1260
+ onProgress: (phase, message) => {
1261
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1262
+ },
1263
+ });
1264
+
1265
+ console.log();
1266
+ if (result.success) {
1267
+ // Update README with project description on completion
1268
+ await updateReadmeOnCompletion(
1269
+ state.projectDir,
1270
+ status.state.name,
1271
+ status.state.idea,
1272
+ status.state.language
1273
+ );
1274
+
1275
+ printSuccess('Workflow completed!');
1276
+ console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
1277
+ } else {
1278
+ printError(result.error || 'Workflow failed');
1279
+ printInfo('You can run /resume again with additional guidance');
1280
+ }
432
1281
  return;
433
1282
  }
434
1283
 
435
- const status = await getWorkflowStatus(state.projectDir);
1284
+ // No formal project state - try to discover context from docs/
1285
+ printInfo('No project state found in selected directory. Scanning for project context...');
1286
+ console.log();
436
1287
 
437
- if (!status.exists) {
438
- printError('No project found to resume');
1288
+ const discovered = await discoverProjectContext(state.projectDir);
1289
+
1290
+ if (!discovered.found) {
1291
+ console.log(theme.secondary(' No project context found in this directory.'));
1292
+ console.log();
1293
+ console.log(theme.dim(' To start a new project, simply type your idea:'));
1294
+ console.log(theme.dim(' Example: "Build a REST API for task management"'));
1295
+ console.log();
1296
+ console.log(theme.dim(' Or navigate to a directory with existing plans:'));
1297
+ console.log(theme.dim(' - docs/PLAN.md or docs/PLAN-DRAFT.md'));
1298
+ console.log(theme.dim(' - README.md'));
439
1299
  return;
440
1300
  }
441
1301
 
1302
+ // Show what we discovered
1303
+ console.log(theme.primary.bold(' Discovered Project Context:'));
442
1304
  console.log();
443
- printInfo('Resuming workflow...');
1305
+
1306
+ if (discovered.name) {
1307
+ console.log(` ${theme.dim('Name:')} ${discovered.name}`);
1308
+ }
1309
+
1310
+ if (discovered.language) {
1311
+ console.log(` ${theme.dim('Language:')} ${theme.primary(discovered.language)}`);
1312
+ }
1313
+
1314
+ if (discovered.planFile) {
1315
+ console.log(` ${theme.dim('Plan:')} docs/${discovered.planFile}`);
1316
+ }
1317
+
1318
+ if (discovered.hasCode) {
1319
+ console.log(` ${theme.dim('Code:')} ${discovered.codeFiles.length} source files found`);
1320
+ }
1321
+
1322
+ if (discovered.idea) {
1323
+ console.log();
1324
+ console.log(theme.secondary(' Project Overview:'));
1325
+ const ideaLines = discovered.idea.split('\n').slice(0, 4);
1326
+ for (const line of ideaLines) {
1327
+ console.log(` ${theme.dim(line.slice(0, 80))}`);
1328
+ }
1329
+ if (discovered.idea.split('\n').length > 4) {
1330
+ console.log(theme.dim(' ...'));
1331
+ }
1332
+ }
1333
+
1334
+ if (discovered.plan) {
1335
+ // Show plan summary
1336
+ console.log();
1337
+ console.log(theme.secondary(' Plan Summary:'));
1338
+ const planLines = discovered.plan.split('\n').filter(l => l.trim().startsWith('#') || l.trim().startsWith('-')).slice(0, 8);
1339
+ for (const line of planLines) {
1340
+ console.log(` ${theme.dim(line.slice(0, 80))}`);
1341
+ }
1342
+ }
1343
+
444
1344
  console.log();
445
1345
 
446
- const result = await resumeWorkflow(state.projectDir, {
1346
+ // Ask user what they want to do
1347
+ const action = await promptSelection(
1348
+ 'What would you like to do?',
1349
+ [
1350
+ { value: 'continue', label: 'Continue with this plan - use existing and continue development' },
1351
+ { value: 'refine', label: 'Refine the plan - review and improve with consensus' },
1352
+ { value: 'new', label: 'Start fresh - provide a new idea' },
1353
+ { value: 'cancel', label: 'Cancel' },
1354
+ ],
1355
+ 'continue'
1356
+ );
1357
+
1358
+ if (action === 'cancel') {
1359
+ printInfo('Cancelled');
1360
+ return;
1361
+ }
1362
+
1363
+ if (action === 'new') {
1364
+ console.log();
1365
+ printInfo('Type your project idea to start a new workflow');
1366
+ return;
1367
+ }
1368
+
1369
+ // Get additional context/guidance
1370
+ let additionalContext = args.join(' ').trim();
1371
+
1372
+ if (!additionalContext && action === 'refine') {
1373
+ additionalContext = await promptForContext(
1374
+ 'What changes or improvements would you like? (e.g., "Add authentication", "Simplify the architecture")'
1375
+ );
1376
+ } else if (!additionalContext) {
1377
+ console.log();
1378
+ const wantsContext = await promptYesNo(
1379
+ theme.primary('Would you like to add any guidance?'),
1380
+ false
1381
+ );
1382
+
1383
+ if (wantsContext) {
1384
+ additionalContext = await promptForContext(
1385
+ 'What guidance would you like to give?'
1386
+ );
1387
+ }
1388
+ }
1389
+
1390
+ // Create project spec from discovered context
1391
+ const projectName = discovered.name ||
1392
+ path.basename(state.projectDir)
1393
+ .toLowerCase()
1394
+ .replace(/[^a-z0-9]/g, '-')
1395
+ .substring(0, 30) ||
1396
+ 'my-project';
1397
+
1398
+ const spec: ProjectSpec = {
1399
+ idea: discovered.idea || discovered.plan?.slice(0, 500) || `Continue developing ${projectName}`,
1400
+ name: projectName,
1401
+ language: discovered.language || state.language,
1402
+ openaiModel: state.model,
1403
+ outputDir: state.projectDir,
1404
+ };
1405
+
1406
+ console.log();
1407
+ printInfo(`Starting workflow for "${projectName}"...`);
1408
+ if (additionalContext) {
1409
+ console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
1410
+ }
1411
+ console.log();
1412
+
1413
+ // Run the workflow
1414
+ const result = await runWorkflow(spec, {
1415
+ projectDir: state.projectDir,
1416
+ consensusConfig: {
1417
+ reviewer: state.reviewer,
1418
+ arbitrator: state.arbitrator,
1419
+ enableArbitration: state.enableArbitration,
1420
+ },
447
1421
  onProgress: (phase, message) => {
448
1422
  console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
449
1423
  },
@@ -451,9 +1425,144 @@ async function handleResume(state: SessionState): Promise<void> {
451
1425
 
452
1426
  console.log();
453
1427
  if (result.success) {
1428
+ // Update README with project description on completion
1429
+ if (state.projectDir) {
1430
+ await updateReadmeOnCompletion(
1431
+ state.projectDir,
1432
+ spec.name || 'my-project',
1433
+ spec.idea,
1434
+ spec.language
1435
+ );
1436
+ }
1437
+
454
1438
  printSuccess('Workflow completed!');
1439
+ console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
455
1440
  } else {
456
1441
  printError(result.error || 'Workflow failed');
1442
+ printInfo('You can run /resume again with additional guidance');
1443
+ }
1444
+ }
1445
+
1446
+ /**
1447
+ * Generate a meaningful project name from an idea
1448
+ * Extracts key nouns and creates a kebab-case name
1449
+ */
1450
+ function generateProjectName(idea: string): string {
1451
+ // Common words to filter out
1452
+ const stopWords = new Set([
1453
+ 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
1454
+ 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
1455
+ 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
1456
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought',
1457
+ 'used', 'create', 'build', 'make', 'develop', 'write', 'implement',
1458
+ 'want', 'like', 'please', 'help', 'me', 'i', 'my', 'we', 'our', 'you',
1459
+ 'your', 'that', 'which', 'who', 'what', 'where', 'when', 'why', 'how',
1460
+ 'this', 'these', 'those', 'it', 'its', 'simple', 'basic', 'new',
1461
+ ]);
1462
+
1463
+ // Extract meaningful words
1464
+ const words = idea
1465
+ .toLowerCase()
1466
+ .replace(/[^a-z0-9\s]/g, ' ')
1467
+ .split(/\s+/)
1468
+ .filter(word => word.length > 2 && !stopWords.has(word));
1469
+
1470
+ // Take first 2-3 meaningful words
1471
+ const nameWords = words.slice(0, 3);
1472
+
1473
+ if (nameWords.length === 0) {
1474
+ // Fallback: use first words from original idea
1475
+ const fallback = idea
1476
+ .toLowerCase()
1477
+ .replace(/[^a-z0-9\s]/g, '')
1478
+ .split(/\s+/)
1479
+ .filter(w => w.length > 0)
1480
+ .slice(0, 2);
1481
+ return fallback.join('-') || 'my-project';
1482
+ }
1483
+
1484
+ return nameWords.join('-').substring(0, 40);
1485
+ }
1486
+
1487
+ /**
1488
+ * Update README.md with project description and usage instructions
1489
+ */
1490
+ async function updateReadmeOnCompletion(
1491
+ projectDir: string,
1492
+ projectName: string,
1493
+ idea: string,
1494
+ language: OutputLanguage
1495
+ ): Promise<void> {
1496
+ const readmePath = path.join(projectDir, 'README.md');
1497
+
1498
+ try {
1499
+ // Read existing README
1500
+ let content = await fs.readFile(readmePath, 'utf-8');
1501
+
1502
+ // Check if it still has the placeholder description
1503
+ if (content.includes('Generated by Popeye CLI')) {
1504
+ // Generate a better description based on the idea
1505
+ const description = `${idea}\n\nThis project was automatically generated and implemented using [Popeye CLI](https://github.com/popeye-cli).`;
1506
+
1507
+ // Replace the placeholder
1508
+ content = content.replace(
1509
+ /Generated by Popeye CLI/g,
1510
+ description
1511
+ );
1512
+
1513
+ // Add a "Getting Started" section if it doesn't exist
1514
+ if (!content.includes('## Getting Started')) {
1515
+ const gettingStarted = language === 'python'
1516
+ ? `
1517
+ ## Getting Started
1518
+
1519
+ 1. Create and activate a virtual environment:
1520
+ \`\`\`bash
1521
+ python -m venv venv
1522
+ source venv/bin/activate # On Windows: venv\\Scripts\\activate
1523
+ \`\`\`
1524
+
1525
+ 2. Install dependencies:
1526
+ \`\`\`bash
1527
+ pip install -e ".[dev]"
1528
+ \`\`\`
1529
+
1530
+ 3. Run the application:
1531
+ \`\`\`bash
1532
+ python -m src.${projectName.replace(/-/g, '_')}.main
1533
+ \`\`\`
1534
+ `
1535
+ : `
1536
+ ## Getting Started
1537
+
1538
+ 1. Install dependencies:
1539
+ \`\`\`bash
1540
+ npm install
1541
+ \`\`\`
1542
+
1543
+ 2. Build the project:
1544
+ \`\`\`bash
1545
+ npm run build
1546
+ \`\`\`
1547
+
1548
+ 3. Run the application:
1549
+ \`\`\`bash
1550
+ npm start
1551
+ \`\`\`
1552
+ `;
1553
+
1554
+ // Insert before "## Development" or at the end
1555
+ if (content.includes('## Development')) {
1556
+ content = content.replace('## Development', gettingStarted + '\n## Development');
1557
+ } else {
1558
+ content += gettingStarted;
1559
+ }
1560
+ }
1561
+
1562
+ await fs.writeFile(readmePath, content, 'utf-8');
1563
+ }
1564
+ } catch {
1565
+ // Silently ignore if README doesn't exist or can't be updated
457
1566
  }
458
1567
  }
459
1568
 
@@ -461,6 +1570,29 @@ async function handleResume(state: SessionState): Promise<void> {
461
1570
  * Handle project idea input
462
1571
  */
463
1572
  async function handleIdea(idea: string, state: SessionState): Promise<void> {
1573
+ const cwd = state.projectDir || process.cwd();
1574
+
1575
+ // Check for existing Popeye projects in the current directory
1576
+ const { all: existingProjects } = await discoverProjects(cwd);
1577
+ const localProjects = existingProjects.filter(p => p.path.startsWith(cwd));
1578
+
1579
+ if (localProjects.length > 0) {
1580
+ console.log();
1581
+ printWarning('Existing Popeye projects found in this directory:');
1582
+ console.log();
1583
+
1584
+ for (const project of localProjects.slice(0, 5)) {
1585
+ const display = formatProjectForDisplay(project);
1586
+ console.log(` ${theme.primary(display.name)} ${theme.dim(`(${display.age})`)}`);
1587
+ console.log(` ${theme.dim(display.status)} - ${theme.dim(project.path)}`);
1588
+ console.log();
1589
+ }
1590
+
1591
+ printInfo('Consider running /resume to continue an existing project.');
1592
+ printInfo('To start a new project anyway, run: /new ' + idea);
1593
+ return;
1594
+ }
1595
+
464
1596
  if (!state.claudeAuth || !state.openaiAuth) {
465
1597
  console.log();
466
1598
  printError('Authentication required');
@@ -474,36 +1606,117 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
474
1606
  }
475
1607
  }
476
1608
 
1609
+ // Generate a meaningful project name
1610
+ const projectName = generateProjectName(idea);
1611
+ const projectDir = path.join(cwd, projectName);
1612
+
477
1613
  console.log();
478
1614
  console.log(theme.primary.bold(' Creating Project'));
479
1615
  console.log(` ${theme.dim('Idea:')} ${idea}`);
1616
+ console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
480
1617
  console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
481
1618
  console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
482
1619
  console.log();
483
1620
 
484
- // Generate project name from idea
485
- const projectName = idea
486
- .toLowerCase()
487
- .replace(/[^a-z0-9\s]/g, '')
488
- .split(/\s+/)
489
- .slice(0, 3)
490
- .join('-')
491
- .substring(0, 30) || 'my-project';
1621
+ const spec: ProjectSpec = {
1622
+ idea,
1623
+ name: projectName,
1624
+ language: state.language,
1625
+ openaiModel: state.model,
1626
+ outputDir: cwd,
1627
+ };
1628
+
1629
+ // Generate scaffold
1630
+ startSpinner('Creating project structure...');
1631
+ const scaffoldResult = await generateProject(spec, cwd);
1632
+
1633
+ if (!scaffoldResult.success) {
1634
+ failSpinner('Scaffolding failed');
1635
+ printError(scaffoldResult.error || 'Failed to create project');
1636
+ return;
1637
+ }
1638
+
1639
+ succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
1640
+
1641
+ // Run workflow with reviewer/arbitrator settings
1642
+ console.log();
1643
+ printInfo('Starting AI workflow...');
1644
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
1645
+ if (state.enableArbitration) {
1646
+ console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
1647
+ }
1648
+ console.log();
1649
+
1650
+ const workflowResult = await runWorkflow(spec, {
1651
+ projectDir,
1652
+ consensusConfig: {
1653
+ reviewer: state.reviewer,
1654
+ arbitrator: state.arbitrator,
1655
+ enableArbitration: state.enableArbitration,
1656
+ geminiModel: state.geminiModel,
1657
+ },
1658
+ onProgress: (phase, message) => {
1659
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1660
+ },
1661
+ });
492
1662
 
493
- const path = await import('node:path');
494
- const projectDir = path.join(state.projectDir || process.cwd(), projectName);
1663
+ stopSpinner();
1664
+
1665
+ console.log();
1666
+ if (workflowResult.success) {
1667
+ // Update README with project description
1668
+ await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
1669
+
1670
+ printSuccess('Project created successfully!');
1671
+ console.log(` ${theme.dim('Location:')} ${projectDir}`);
1672
+ state.projectDir = projectDir;
1673
+ } else {
1674
+ printError(workflowResult.error || 'Workflow failed');
1675
+ }
1676
+ }
1677
+
1678
+ /**
1679
+ * Handle /new command - force create a new project (skips existing project check)
1680
+ */
1681
+ async function handleNewProject(idea: string, state: SessionState): Promise<void> {
1682
+ if (!state.claudeAuth || !state.openaiAuth) {
1683
+ console.log();
1684
+ printError('Authentication required');
1685
+ printInfo('Running authentication flow...');
1686
+ console.log();
1687
+
1688
+ const authenticated = await ensureAuthentication(state);
1689
+ if (!authenticated) {
1690
+ printWarning('Skipping project creation - authentication incomplete');
1691
+ return;
1692
+ }
1693
+ }
1694
+
1695
+ const cwd = state.projectDir || process.cwd();
1696
+
1697
+ // Generate a meaningful project name
1698
+ const projectName = generateProjectName(idea);
1699
+ const projectDir = path.join(cwd, projectName);
1700
+
1701
+ console.log();
1702
+ console.log(theme.primary.bold(' Creating New Project'));
1703
+ console.log(` ${theme.dim('Idea:')} ${idea}`);
1704
+ console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
1705
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
1706
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
1707
+ console.log();
495
1708
 
496
1709
  const spec: ProjectSpec = {
497
1710
  idea,
498
1711
  name: projectName,
499
1712
  language: state.language,
500
1713
  openaiModel: state.model,
501
- outputDir: state.projectDir || process.cwd(),
1714
+ outputDir: cwd,
502
1715
  };
503
1716
 
504
1717
  // Generate scaffold
505
1718
  startSpinner('Creating project structure...');
506
- const scaffoldResult = await generateProject(spec, state.projectDir || process.cwd());
1719
+ const scaffoldResult = await generateProject(spec, cwd);
507
1720
 
508
1721
  if (!scaffoldResult.success) {
509
1722
  failSpinner('Scaffolding failed');
@@ -513,13 +1726,23 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
513
1726
 
514
1727
  succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
515
1728
 
516
- // Run workflow
1729
+ // Run workflow with reviewer/arbitrator settings
517
1730
  console.log();
518
1731
  printInfo('Starting AI workflow...');
1732
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
1733
+ if (state.enableArbitration) {
1734
+ console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
1735
+ }
519
1736
  console.log();
520
1737
 
521
1738
  const workflowResult = await runWorkflow(spec, {
522
1739
  projectDir,
1740
+ consensusConfig: {
1741
+ reviewer: state.reviewer,
1742
+ arbitrator: state.arbitrator,
1743
+ enableArbitration: state.enableArbitration,
1744
+ geminiModel: state.geminiModel,
1745
+ },
523
1746
  onProgress: (phase, message) => {
524
1747
  console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
525
1748
  },
@@ -529,6 +1752,9 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
529
1752
 
530
1753
  console.log();
531
1754
  if (workflowResult.success) {
1755
+ // Update README with project description
1756
+ await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
1757
+
532
1758
  printSuccess('Project created successfully!');
533
1759
  console.log(` ${theme.dim('Location:')} ${projectDir}`);
534
1760
  state.projectDir = projectDir;
@@ -543,20 +1769,36 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
543
1769
  export async function startInteractiveMode(): Promise<void> {
544
1770
  console.clear();
545
1771
 
546
- // Initialize state
1772
+ // Initialize state from saved config
547
1773
  const config = await loadConfig();
548
1774
  const state: SessionState = {
549
1775
  projectDir: process.cwd(),
550
1776
  language: config.project.default_language,
551
1777
  model: config.apis.openai.model,
1778
+ geminiModel: 'gemini-2.0-flash',
552
1779
  claudeAuth: false,
553
1780
  openaiAuth: false,
1781
+ geminiAuth: false,
1782
+ // Load saved reviewer/arbitrator settings from config
1783
+ reviewer: config.consensus.reviewer,
1784
+ arbitrator: config.consensus.arbitrator === 'off' ? 'openai' : config.consensus.arbitrator,
1785
+ enableArbitration: config.consensus.enable_arbitration,
554
1786
  };
555
1787
 
556
1788
  // Draw header
557
1789
  drawHeader();
558
1790
  console.log();
559
1791
 
1792
+ // Show how Popeye works
1793
+ console.log(theme.secondary(' How Popeye works:'));
1794
+ console.log(theme.dim(' ├─ ') + theme.primary('Claude Code CLI') + theme.dim(' - Generates code (uses your model & MCP settings)'));
1795
+ console.log(theme.dim(' ├─ ') + theme.secondary('Reviewer (configurable)') + theme.dim(' - Reviews plans until consensus'));
1796
+ console.log(theme.dim(' └─ ') + theme.secondary('Arbitrator (optional)') + theme.dim(' - Breaks deadlocks when stuck'));
1797
+ console.log();
1798
+ console.log(theme.dim(' You can choose OpenAI or Gemini as reviewer/arbitrator during setup.'));
1799
+ console.log(theme.dim(' Plans are saved to docs/ folder in markdown format.'));
1800
+ console.log();
1801
+
560
1802
  // Check and perform authentication
561
1803
  const isAuthenticated = await ensureAuthentication(state);
562
1804
 
@@ -579,11 +1821,11 @@ export async function startInteractiveMode(): Promise<void> {
579
1821
 
580
1822
  // Input loop
581
1823
  const promptUser = (): void => {
582
- drawInputFrame(state);
1824
+ drawInputBoxTop(state);
583
1825
 
584
1826
  rl.question(getPrompt(), async (input) => {
585
- // Clear the input frame line
586
- process.stdout.write('\x1b[1A\x1b[2K'); // Move up and clear
1827
+ // Draw bottom of input box after user presses enter
1828
+ drawInputBoxBottom();
587
1829
 
588
1830
  const shouldContinue = await handleInput(input, state);
589
1831