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