popeye-cli 2.0.0 → 2.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 (161) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/CONTRIBUTING.md +23 -2
  3. package/README.md +47 -18
  4. package/dist/adapters/gemini.js +3 -3
  5. package/dist/adapters/openai.js +2 -2
  6. package/dist/adapters/openai.js.map +1 -1
  7. package/dist/auth/gemini.js +1 -1
  8. package/dist/cli/commands/create.d.ts.map +1 -1
  9. package/dist/cli/commands/create.js +11 -5
  10. package/dist/cli/commands/create.js.map +1 -1
  11. package/dist/cli/commands/resume.d.ts.map +1 -1
  12. package/dist/cli/commands/resume.js +9 -1
  13. package/dist/cli/commands/resume.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +33 -4
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/config/defaults.d.ts.map +1 -1
  18. package/dist/config/defaults.js +7 -2
  19. package/dist/config/defaults.js.map +1 -1
  20. package/dist/config/index.d.ts +1 -7
  21. package/dist/config/index.d.ts.map +1 -1
  22. package/dist/config/popeye-md.d.ts +32 -0
  23. package/dist/config/popeye-md.d.ts.map +1 -0
  24. package/dist/config/popeye-md.js +111 -0
  25. package/dist/config/popeye-md.js.map +1 -0
  26. package/dist/config/schema.d.ts +3 -21
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +21 -8
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/generators/all.d.ts.map +1 -1
  31. package/dist/generators/all.js +23 -1
  32. package/dist/generators/all.js.map +1 -1
  33. package/dist/pipeline/artifact-manager.d.ts.map +1 -1
  34. package/dist/pipeline/artifact-manager.js +3 -0
  35. package/dist/pipeline/artifact-manager.js.map +1 -1
  36. package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
  37. package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
  38. package/dist/pipeline/bridges/review-bridge.js +266 -0
  39. package/dist/pipeline/bridges/review-bridge.js.map +1 -0
  40. package/dist/pipeline/consensus/consensus-runner.js +3 -3
  41. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  42. package/dist/pipeline/gate-engine.js +1 -1
  43. package/dist/pipeline/gate-engine.js.map +1 -1
  44. package/dist/pipeline/migration.d.ts.map +1 -1
  45. package/dist/pipeline/migration.js +3 -26
  46. package/dist/pipeline/migration.js.map +1 -1
  47. package/dist/pipeline/orchestrator.d.ts +2 -0
  48. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  49. package/dist/pipeline/orchestrator.js +10 -1
  50. package/dist/pipeline/orchestrator.js.map +1 -1
  51. package/dist/pipeline/phases/implementation.d.ts.map +1 -1
  52. package/dist/pipeline/phases/implementation.js +5 -2
  53. package/dist/pipeline/phases/implementation.js.map +1 -1
  54. package/dist/pipeline/phases/intake.d.ts +1 -0
  55. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  56. package/dist/pipeline/phases/intake.js +56 -8
  57. package/dist/pipeline/phases/intake.js.map +1 -1
  58. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  59. package/dist/pipeline/phases/recovery-loop.js +2 -0
  60. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  61. package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
  62. package/dist/pipeline/phases/role-planning.js +2 -3
  63. package/dist/pipeline/phases/role-planning.js.map +1 -1
  64. package/dist/pipeline/skills/constitution-generator.d.ts +51 -0
  65. package/dist/pipeline/skills/constitution-generator.d.ts.map +1 -0
  66. package/dist/pipeline/skills/constitution-generator.js +210 -0
  67. package/dist/pipeline/skills/constitution-generator.js.map +1 -0
  68. package/dist/pipeline/skills/generator.d.ts +65 -0
  69. package/dist/pipeline/skills/generator.d.ts.map +1 -0
  70. package/dist/pipeline/skills/generator.js +221 -0
  71. package/dist/pipeline/skills/generator.js.map +1 -0
  72. package/dist/pipeline/skills/role-map.d.ts +38 -0
  73. package/dist/pipeline/skills/role-map.d.ts.map +1 -0
  74. package/dist/pipeline/skills/role-map.js +234 -0
  75. package/dist/pipeline/skills/role-map.js.map +1 -0
  76. package/dist/pipeline/skills/types.d.ts +47 -0
  77. package/dist/pipeline/skills/types.d.ts.map +1 -0
  78. package/dist/pipeline/skills/types.js +5 -0
  79. package/dist/pipeline/skills/types.js.map +1 -0
  80. package/dist/pipeline/type-defs/artifacts.d.ts +10 -0
  81. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  82. package/dist/pipeline/type-defs/artifacts.js +2 -0
  83. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  84. package/dist/pipeline/type-defs/audit.d.ts +6 -0
  85. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  86. package/dist/pipeline/type-defs/checks.d.ts +2 -0
  87. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  88. package/dist/pipeline/type-defs/packets.d.ts +30 -0
  89. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  90. package/dist/pipeline/type-defs/state.d.ts +11 -0
  91. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  92. package/dist/pipeline/type-defs/state.js +2 -0
  93. package/dist/pipeline/type-defs/state.js.map +1 -1
  94. package/dist/types/consensus.d.ts +5 -1
  95. package/dist/types/consensus.d.ts.map +1 -1
  96. package/dist/types/consensus.js +15 -4
  97. package/dist/types/consensus.js.map +1 -1
  98. package/dist/types/index.d.ts +1 -1
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/index.js +1 -1
  101. package/dist/types/index.js.map +1 -1
  102. package/dist/types/project.d.ts +1 -1
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +39 -10
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +1 -7
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +1 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/upgrade/handlers.js +5 -5
  111. package/dist/upgrade/handlers.js.map +1 -1
  112. package/dist/workflow/index.d.ts.map +1 -1
  113. package/dist/workflow/index.js +18 -14
  114. package/dist/workflow/index.js.map +1 -1
  115. package/dist/workflow/website-strategy.js +1 -1
  116. package/dist/workflow/website-strategy.js.map +1 -1
  117. package/package.json +1 -1
  118. package/src/adapters/gemini.ts +3 -3
  119. package/src/adapters/openai.ts +2 -2
  120. package/src/auth/gemini.ts +1 -1
  121. package/src/cli/commands/create.ts +12 -6
  122. package/src/cli/commands/resume.ts +9 -1
  123. package/src/cli/interactive.ts +36 -4
  124. package/src/config/defaults.ts +7 -2
  125. package/src/config/popeye-md.ts +139 -0
  126. package/src/config/schema.ts +21 -8
  127. package/src/generators/all.ts +23 -1
  128. package/src/pipeline/artifact-manager.ts +3 -0
  129. package/src/pipeline/bridges/review-bridge.ts +371 -0
  130. package/src/pipeline/consensus/consensus-runner.ts +3 -3
  131. package/src/pipeline/gate-engine.ts +1 -1
  132. package/src/pipeline/migration.ts +5 -30
  133. package/src/pipeline/orchestrator.ts +14 -0
  134. package/src/pipeline/phases/implementation.ts +6 -2
  135. package/src/pipeline/phases/intake.ts +73 -10
  136. package/src/pipeline/phases/recovery-loop.ts +2 -0
  137. package/src/pipeline/phases/role-planning.ts +2 -3
  138. package/src/pipeline/skills/constitution-generator.ts +236 -0
  139. package/src/pipeline/skills/generator.ts +287 -0
  140. package/src/pipeline/skills/role-map.ts +248 -0
  141. package/src/pipeline/skills/types.ts +53 -0
  142. package/src/pipeline/type-defs/artifacts.ts +2 -0
  143. package/src/pipeline/type-defs/state.ts +2 -0
  144. package/src/types/consensus.ts +16 -4
  145. package/src/types/index.ts +1 -0
  146. package/src/types/project.ts +39 -10
  147. package/src/types/workflow.ts +1 -1
  148. package/src/upgrade/handlers.ts +5 -5
  149. package/src/workflow/index.ts +18 -14
  150. package/src/workflow/website-strategy.ts +1 -1
  151. package/tests/cli/model-command.test.ts +19 -9
  152. package/tests/config/config.test.ts +3 -3
  153. package/tests/config/popeye-md.test.ts +168 -0
  154. package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
  155. package/tests/pipeline/migration.test.ts +4 -3
  156. package/tests/pipeline/session-guidance.test.ts +205 -0
  157. package/tests/pipeline/skills/constitution-generator.test.ts +201 -0
  158. package/tests/pipeline/skills/generator.test.ts +213 -0
  159. package/tests/pipeline/skills/role-map.test.ts +198 -0
  160. package/tests/types/consensus.test.ts +1 -1
  161. package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
@@ -242,7 +242,12 @@ export interface WebsiteSpec {
242
242
  /**
243
243
  * Known OpenAI models (used for suggestions and display, not strict validation)
244
244
  */
245
- export const KNOWN_OPENAI_MODELS = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'] as const;
245
+ export const KNOWN_OPENAI_MODELS = [
246
+ 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano',
247
+ 'o3', 'o3-mini', 'o4-mini',
248
+ 'gpt-4o', 'gpt-4o-mini',
249
+ 'gpt-4-turbo', 'o1-preview', 'o1-mini',
250
+ ] as const;
246
251
 
247
252
  /**
248
253
  * OpenAI model schema - accepts any non-empty string to support new models
@@ -289,24 +294,48 @@ export interface GenerationOptions {
289
294
  * Available OpenAI models with descriptions
290
295
  */
291
296
  export const OPENAI_MODELS: Record<OpenAIModel, { description: string; recommended: string }> = {
292
- 'gpt-4o': {
293
- description: 'Most capable, best reasoning',
297
+ 'gpt-4.1': {
298
+ description: 'Smartest non-reasoning model, 1M context',
294
299
  recommended: 'Complex projects',
295
300
  },
301
+ 'gpt-4.1-mini': {
302
+ description: 'Fast, strong instruction following',
303
+ recommended: 'Medium complexity',
304
+ },
305
+ 'gpt-4.1-nano': {
306
+ description: 'Fastest, most cost-efficient',
307
+ recommended: 'Simple projects',
308
+ },
309
+ 'o3': {
310
+ description: 'Strongest reasoning model',
311
+ recommended: 'Architectural decisions',
312
+ },
313
+ 'o3-mini': {
314
+ description: 'Efficient reasoning',
315
+ recommended: 'Code review',
316
+ },
317
+ 'o4-mini': {
318
+ description: 'Fast reasoning, best on STEM',
319
+ recommended: 'Technical analysis',
320
+ },
321
+ 'gpt-4o': {
322
+ description: 'Multimodal, strong all-rounder',
323
+ recommended: 'General purpose',
324
+ },
296
325
  'gpt-4o-mini': {
297
326
  description: 'Fast, cost-effective',
298
- recommended: 'Simple projects',
327
+ recommended: 'Simple tasks',
299
328
  },
300
329
  'gpt-4-turbo': {
301
- description: 'High capability, faster',
302
- recommended: 'Medium complexity',
330
+ description: 'High capability, legacy',
331
+ recommended: 'Backward compatibility',
303
332
  },
304
333
  'o1-preview': {
305
- description: 'Advanced reasoning',
306
- recommended: 'Architectural decisions',
334
+ description: 'Advanced reasoning (legacy)',
335
+ recommended: 'Legacy reasoning tasks',
307
336
  },
308
337
  'o1-mini': {
309
- description: 'Efficient reasoning',
310
- recommended: 'Code review',
338
+ description: 'Efficient reasoning (legacy)',
339
+ recommended: 'Legacy reasoning tasks',
311
340
  },
312
341
  };
@@ -254,7 +254,7 @@ export const ProjectStateSchema = z.object({
254
254
  name: z.string(),
255
255
  idea: z.string(),
256
256
  language: OutputLanguageSchema,
257
- openaiModel: z.enum(['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini']),
257
+ openaiModel: z.string().min(1),
258
258
  phase: WorkflowPhaseSchema,
259
259
  status: ProjectStatusSchema,
260
260
  specification: z.string().optional(),
@@ -349,7 +349,7 @@ export async function upgradeFullstackToAll(
349
349
  idea: 'Marketing website',
350
350
  name: projectName,
351
351
  language: 'all',
352
- openaiModel: 'gpt-4o',
352
+ openaiModel: 'gpt-4.1',
353
353
  };
354
354
 
355
355
  // Build content context from user docs, brand assets, and strategy
@@ -421,7 +421,7 @@ export async function upgradeSingleToFullstack(
421
421
  if (!(await pathExists(frontendDir))) {
422
422
  const spec: ProjectSpec = {
423
423
  idea: 'Frontend application', name: projectName,
424
- language: 'fullstack', openaiModel: 'gpt-4o',
424
+ language: 'fullstack', openaiModel: 'gpt-4.1',
425
425
  };
426
426
  const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
427
427
  baseDir: frontendDir,
@@ -439,7 +439,7 @@ export async function upgradeSingleToFullstack(
439
439
  if (!(await pathExists(backendDir))) {
440
440
  const spec: ProjectSpec = {
441
441
  idea: 'Backend API', name: projectName,
442
- language: 'fullstack', openaiModel: 'gpt-4o',
442
+ language: 'fullstack', openaiModel: 'gpt-4.1',
443
443
  };
444
444
  const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
445
445
  baseDir: backendDir,
@@ -508,7 +508,7 @@ export async function upgradeWebsiteToAll(
508
508
  if (!(await pathExists(frontendDir))) {
509
509
  const spec: ProjectSpec = {
510
510
  idea: 'Frontend application', name: projectName,
511
- language: 'all', openaiModel: 'gpt-4o',
511
+ language: 'all', openaiModel: 'gpt-4.1',
512
512
  };
513
513
  const result = await generateTypeScriptProject(spec, path.join(projectDir, 'apps'), {
514
514
  baseDir: frontendDir,
@@ -520,7 +520,7 @@ export async function upgradeWebsiteToAll(
520
520
  if (!(await pathExists(backendDir))) {
521
521
  const spec: ProjectSpec = {
522
522
  idea: 'Backend API', name: projectName,
523
- language: 'all', openaiModel: 'gpt-4o',
523
+ language: 'all', openaiModel: 'gpt-4.1',
524
524
  };
525
525
  const result = await generatePythonProject(spec, path.join(projectDir, 'apps'), {
526
526
  baseDir: backendDir,
@@ -104,21 +104,24 @@ export async function runWorkflow(
104
104
  const useLegacy = process.env.POPEYE_LEGACY_WORKFLOW === '1' || process.env.POPEYE_LEGACY_WORKFLOW === 'true';
105
105
  if (!useLegacy) {
106
106
  try {
107
- const state = await loadProject(projectDir).catch(() => null);
108
- if (state) {
109
- const result = await runPipeline({
110
- projectDir,
111
- state,
112
- consensusConfig,
113
- onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
114
- onProgress: (msg) => onProgress?.('pipeline', msg),
115
- });
116
- return {
117
- success: result.success,
118
- state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
119
- error: result.error,
120
- };
107
+ // Bootstrap state if it doesn't exist yet (new projects)
108
+ let state = await loadProject(projectDir).catch(() => null);
109
+ if (!state) {
110
+ const { createProject } = await import('../state/index.js');
111
+ state = await createProject(spec, projectDir);
121
112
  }
113
+ const result = await runPipeline({
114
+ projectDir,
115
+ state,
116
+ consensusConfig,
117
+ onPhaseStart: (phase) => onProgress?.('pipeline', `Starting phase: ${phase}`),
118
+ onProgress: (msg) => onProgress?.('pipeline', msg),
119
+ });
120
+ return {
121
+ success: result.success,
122
+ state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
123
+ error: result.error,
124
+ };
122
125
  } catch {
123
126
  // Fall through to legacy workflow on pipeline error
124
127
  onProgress?.('workflow', 'Pipeline mode failed, falling back to legacy workflow...');
@@ -241,6 +244,7 @@ export async function resumeWorkflow(
241
244
  projectDir,
242
245
  state,
243
246
  consensusConfig,
247
+ additionalContext,
244
248
  onPhaseStart: (phase) => onProgress?.('pipeline', `Resuming phase: ${phase}`),
245
249
  onProgress: (msg) => onProgress?.('pipeline', msg),
246
250
  });
@@ -142,7 +142,7 @@ Respond with ONLY valid JSON, no markdown code fences or explanation.`;
142
142
  onProgress?.('Generating website strategy via AI...');
143
143
 
144
144
  const completion = await client.chat.completions.create({
145
- model: 'gpt-4o',
145
+ model: 'gpt-4.1',
146
146
  messages: [{ role: 'user', content: prompt }],
147
147
  temperature: 0.4,
148
148
  max_tokens: 4096,
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { describe, it, expect } from 'vitest';
7
7
  import { OpenAIModelSchema, KNOWN_OPENAI_MODELS } from '../../src/types/project.js';
8
- import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS } from '../../src/types/consensus.js';
8
+ import { GeminiModelSchema, GrokModelSchema, KNOWN_GEMINI_MODELS, KNOWN_GROK_MODELS } from '../../src/types/consensus.js';
9
9
 
10
10
  describe('OpenAI model validation', () => {
11
11
  it('should accept known OpenAI models', () => {
@@ -17,7 +17,7 @@ describe('OpenAI model validation', () => {
17
17
  it('should accept unknown/new OpenAI models (flexible)', () => {
18
18
  expect(OpenAIModelSchema.safeParse('gpt-5').success).toBe(true);
19
19
  expect(OpenAIModelSchema.safeParse('gpt-5.2-turbo').success).toBe(true);
20
- expect(OpenAIModelSchema.safeParse('o3-mini').success).toBe(true);
20
+ expect(OpenAIModelSchema.safeParse('some-future-model').success).toBe(true);
21
21
  });
22
22
 
23
23
  it('should reject empty string', () => {
@@ -33,8 +33,8 @@ describe('Gemini model validation', () => {
33
33
  });
34
34
 
35
35
  it('should accept unknown/new Gemini models (flexible)', () => {
36
- expect(GeminiModelSchema.safeParse('gemini-2.5-pro').success).toBe(true);
37
36
  expect(GeminiModelSchema.safeParse('gemini-3.0-ultra').success).toBe(true);
37
+ expect(GeminiModelSchema.safeParse('gemini-4.0-flash').success).toBe(true);
38
38
  });
39
39
 
40
40
  it('should reject empty string', () => {
@@ -44,9 +44,9 @@ describe('Gemini model validation', () => {
44
44
 
45
45
  describe('Grok model validation', () => {
46
46
  it('should accept any non-empty string as Grok model', () => {
47
+ expect(GrokModelSchema.safeParse('grok-4-0709').success).toBe(true);
47
48
  expect(GrokModelSchema.safeParse('grok-3').success).toBe(true);
48
49
  expect(GrokModelSchema.safeParse('grok-3-mini').success).toBe(true);
49
- expect(GrokModelSchema.safeParse('grok-2').success).toBe(true);
50
50
  expect(GrokModelSchema.safeParse('some-future-model').success).toBe(true);
51
51
  });
52
52
 
@@ -62,15 +62,25 @@ describe('Grok model validation', () => {
62
62
 
63
63
  describe('known models lists', () => {
64
64
  it('should have known OpenAI models', () => {
65
+ expect(KNOWN_OPENAI_MODELS).toContain('gpt-4.1');
65
66
  expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o');
66
- expect(KNOWN_OPENAI_MODELS).toContain('gpt-4o-mini');
67
- expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(5);
67
+ expect(KNOWN_OPENAI_MODELS).toContain('o3');
68
+ expect(KNOWN_OPENAI_MODELS).toContain('o4-mini');
69
+ expect(KNOWN_OPENAI_MODELS.length).toBeGreaterThanOrEqual(8);
68
70
  });
69
71
 
70
72
  it('should have known Gemini models', () => {
73
+ expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-flash');
74
+ expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.5-pro');
71
75
  expect(KNOWN_GEMINI_MODELS).toContain('gemini-2.0-flash');
72
- expect(KNOWN_GEMINI_MODELS).toContain('gemini-1.5-pro');
73
- expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(3);
76
+ expect(KNOWN_GEMINI_MODELS.length).toBeGreaterThanOrEqual(5);
77
+ });
78
+
79
+ it('should have known Grok models', () => {
80
+ expect(KNOWN_GROK_MODELS).toContain('grok-4-0709');
81
+ expect(KNOWN_GROK_MODELS).toContain('grok-3');
82
+ expect(KNOWN_GROK_MODELS).toContain('grok-3-mini');
83
+ expect(KNOWN_GROK_MODELS.length).toBeGreaterThanOrEqual(4);
74
84
  });
75
85
  });
76
86
 
@@ -84,7 +94,7 @@ describe('backward compatibility', () => {
84
94
  });
85
95
 
86
96
  it('should not auto-detect non-OpenAI models as known OpenAI', () => {
87
- const nonOpenAI = ['gemini-2.0-flash', 'grok-3'];
97
+ const nonOpenAI = ['gemini-2.5-flash', 'grok-3', 'grok-4-0709'];
88
98
  for (const model of nonOpenAI) {
89
99
  const isKnown = (KNOWN_OPENAI_MODELS as readonly string[]).includes(model);
90
100
  expect(isKnown).toBe(false);
@@ -23,7 +23,7 @@ describe('DEFAULT_CONFIG', () => {
23
23
  });
24
24
 
25
25
  it('should have valid API defaults', () => {
26
- expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-4o');
26
+ expect(DEFAULT_CONFIG.apis.openai.model).toBe('gpt-4.1');
27
27
  expect(DEFAULT_CONFIG.apis.openai.temperature).toBe(0.3);
28
28
  expect(DEFAULT_CONFIG.apis.openai.max_tokens).toBe(4096);
29
29
  });
@@ -122,11 +122,11 @@ describe('ConfigSchema', () => {
122
122
  expect(result.success).toBe(false);
123
123
  });
124
124
 
125
- it('should reject invalid model', () => {
125
+ it('should reject empty model string', () => {
126
126
  const config = {
127
127
  apis: {
128
128
  openai: {
129
- model: 'gpt-5',
129
+ model: '',
130
130
  },
131
131
  },
132
132
  };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Fix C tests — readPopeyeMdConfig shared config reader.
3
+ * Verifies popeye.md parsing for CLI commands.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { readPopeyeMdConfig } from '../../src/config/popeye-md.js';
11
+
12
+ describe('Fix C: readPopeyeMdConfig', () => {
13
+ let tmpDir: string;
14
+
15
+ beforeEach(async () => {
16
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-md-test-'));
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it('should return null when popeye.md does not exist', async () => {
24
+ const config = await readPopeyeMdConfig(tmpDir);
25
+ expect(config).toBeNull();
26
+ });
27
+
28
+ it('should parse basic config with reviewer and language', async () => {
29
+ await fs.writeFile(
30
+ path.join(tmpDir, 'popeye.md'),
31
+ [
32
+ '---',
33
+ 'language: python',
34
+ 'reviewer: openai',
35
+ 'arbitrator: gemini',
36
+ '---',
37
+ '',
38
+ '# Project Config',
39
+ ].join('\n'),
40
+ );
41
+
42
+ const config = await readPopeyeMdConfig(tmpDir);
43
+ expect(config).not.toBeNull();
44
+ expect(config!.language).toBe('python');
45
+ expect(config!.reviewer).toBe('openai');
46
+ expect(config!.arbitrator).toBe('gemini');
47
+ expect(config!.enableArbitration).toBe(true);
48
+ });
49
+
50
+ it('should parse model fields from popeye.md', async () => {
51
+ await fs.writeFile(
52
+ path.join(tmpDir, 'popeye.md'),
53
+ [
54
+ '---',
55
+ 'language: typescript',
56
+ 'reviewer: gemini',
57
+ 'arbitrator: grok',
58
+ 'openaiModel: gpt-4o-mini',
59
+ 'geminiModel: gemini-2.0-flash',
60
+ 'grokModel: grok-3',
61
+ '---',
62
+ ].join('\n'),
63
+ );
64
+
65
+ const config = await readPopeyeMdConfig(tmpDir);
66
+ expect(config).not.toBeNull();
67
+ expect(config!.openaiModel).toBe('gpt-4o-mini');
68
+ expect(config!.geminiModel).toBe('gemini-2.0-flash');
69
+ expect(config!.grokModel).toBe('grok-3');
70
+ });
71
+
72
+ it('should handle arbitrator: off', async () => {
73
+ await fs.writeFile(
74
+ path.join(tmpDir, 'popeye.md'),
75
+ [
76
+ '---',
77
+ 'language: fullstack',
78
+ 'reviewer: openai',
79
+ 'arbitrator: off',
80
+ '---',
81
+ ].join('\n'),
82
+ );
83
+
84
+ const config = await readPopeyeMdConfig(tmpDir);
85
+ expect(config).not.toBeNull();
86
+ expect(config!.enableArbitration).toBe(false);
87
+ });
88
+
89
+ it('should return null when frontmatter is missing', async () => {
90
+ await fs.writeFile(
91
+ path.join(tmpDir, 'popeye.md'),
92
+ '# Just a markdown file\n\nNo frontmatter here.',
93
+ );
94
+
95
+ const config = await readPopeyeMdConfig(tmpDir);
96
+ expect(config).toBeNull();
97
+ });
98
+
99
+ it('should return null when essential fields are missing', async () => {
100
+ await fs.writeFile(
101
+ path.join(tmpDir, 'popeye.md'),
102
+ [
103
+ '---',
104
+ 'projectName: my-app',
105
+ 'created: 2024-01-01',
106
+ '---',
107
+ ].join('\n'),
108
+ );
109
+
110
+ const config = await readPopeyeMdConfig(tmpDir);
111
+ // Missing language and reviewer -> null
112
+ expect(config).toBeNull();
113
+ });
114
+
115
+ it('should extract notes section', async () => {
116
+ await fs.writeFile(
117
+ path.join(tmpDir, 'popeye.md'),
118
+ [
119
+ '---',
120
+ 'language: website',
121
+ 'reviewer: openai',
122
+ '---',
123
+ '',
124
+ '## Notes',
125
+ 'This project uses Tailwind CSS.',
126
+ 'Deploy to Vercel.',
127
+ ].join('\n'),
128
+ );
129
+
130
+ const config = await readPopeyeMdConfig(tmpDir);
131
+ expect(config).not.toBeNull();
132
+ expect(config!.notes).toContain('Tailwind CSS');
133
+ });
134
+
135
+ it('should reject invalid language values', async () => {
136
+ await fs.writeFile(
137
+ path.join(tmpDir, 'popeye.md'),
138
+ [
139
+ '---',
140
+ 'language: rust',
141
+ 'reviewer: openai',
142
+ '---',
143
+ ].join('\n'),
144
+ );
145
+
146
+ const config = await readPopeyeMdConfig(tmpDir);
147
+ // Invalid language means essential field is missing
148
+ expect(config).toBeNull();
149
+ });
150
+
151
+ it('should return model fields as undefined when not specified', async () => {
152
+ await fs.writeFile(
153
+ path.join(tmpDir, 'popeye.md'),
154
+ [
155
+ '---',
156
+ 'language: python',
157
+ 'reviewer: openai',
158
+ '---',
159
+ ].join('\n'),
160
+ );
161
+
162
+ const config = await readPopeyeMdConfig(tmpDir);
163
+ expect(config).not.toBeNull();
164
+ expect(config!.openaiModel).toBeUndefined();
165
+ expect(config!.geminiModel).toBeUndefined();
166
+ expect(config!.grokModel).toBeUndefined();
167
+ });
168
+ });