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