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
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Skill generator tests — prompt building, parsing, rendering, skip logic.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import {
9
+ shouldGenerateSkill,
10
+ buildSkillGenPrompt,
11
+ parseSkillPrompts,
12
+ renderSkillMarkdown,
13
+ writeGenerationMarker,
14
+ } from '../../../src/pipeline/skills/generator.js';
15
+ import type { SkillGenerationContext } from '../../../src/pipeline/skills/types.js';
16
+ import type { RepoSnapshot } from '../../../src/pipeline/types.js';
17
+
18
+ const TEST_DIR = join(process.cwd(), '.test-skill-generator');
19
+ const SKILLS_DIR = join(TEST_DIR, 'skills');
20
+
21
+ function makeSnapshot(): RepoSnapshot {
22
+ return {
23
+ snapshot_id: 'test-snap',
24
+ timestamp: new Date().toISOString(),
25
+ tree_summary: '',
26
+ config_files: [],
27
+ languages_detected: [],
28
+ scripts: {},
29
+ env_files: [],
30
+ migrations_present: false,
31
+ ports_entrypoints: [],
32
+ total_files: 0,
33
+ total_lines: 0,
34
+ };
35
+ }
36
+
37
+ function makeContext(overrides: Partial<SkillGenerationContext> = {}): SkillGenerationContext {
38
+ return {
39
+ language: 'python',
40
+ expandedSpec: 'Build a REST API for task management',
41
+ snapshot: makeSnapshot(),
42
+ activeRoles: ['DISPATCHER', 'ARCHITECT', 'BACKEND_PROGRAMMER', 'DB_EXPERT'],
43
+ skillsDir: SKILLS_DIR,
44
+ projectName: 'TestProject',
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ describe('generator', () => {
50
+ beforeEach(() => {
51
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
52
+ mkdirSync(SKILLS_DIR, { recursive: true });
53
+ });
54
+
55
+ afterEach(() => {
56
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
57
+ });
58
+
59
+ describe('shouldGenerateSkill', () => {
60
+ it('should return true when no .md file exists', () => {
61
+ expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(true);
62
+ });
63
+
64
+ it('should return false when .md file already exists', () => {
65
+ writeFileSync(join(SKILLS_DIR, 'BACKEND_PROGRAMMER.md'), 'existing content');
66
+ expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(false);
67
+ });
68
+
69
+ it('should check per-role independently', () => {
70
+ writeFileSync(join(SKILLS_DIR, 'ARCHITECT.md'), 'existing');
71
+ expect(shouldGenerateSkill(SKILLS_DIR, 'ARCHITECT')).toBe(false);
72
+ expect(shouldGenerateSkill(SKILLS_DIR, 'BACKEND_PROGRAMMER')).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe('buildSkillGenPrompt', () => {
77
+ it('should include project name and tech stack', () => {
78
+ const prompt = buildSkillGenPrompt(
79
+ makeContext(),
80
+ ['BACKEND_PROGRAMMER'],
81
+ { backend: 'FastAPI', language: 'Python 3.11+' },
82
+ );
83
+ expect(prompt).toContain('TestProject');
84
+ expect(prompt).toContain('FastAPI');
85
+ expect(prompt).toContain('Python 3.11+');
86
+ });
87
+
88
+ it('should include role descriptions', () => {
89
+ const prompt = buildSkillGenPrompt(
90
+ makeContext(),
91
+ ['BACKEND_PROGRAMMER', 'DB_EXPERT'],
92
+ { backend: 'FastAPI' },
93
+ );
94
+ expect(prompt).toContain('BACKEND_PROGRAMMER');
95
+ expect(prompt).toContain('DB_EXPERT');
96
+ });
97
+
98
+ it('should include session guidance when present', () => {
99
+ const prompt = buildSkillGenPrompt(
100
+ makeContext({ sessionGuidance: 'Focus on security' }),
101
+ ['BACKEND_PROGRAMMER'],
102
+ { backend: 'FastAPI' },
103
+ );
104
+ expect(prompt).toContain('Focus on security');
105
+ });
106
+
107
+ it('should include expanded spec', () => {
108
+ const prompt = buildSkillGenPrompt(
109
+ makeContext({ expandedSpec: 'Build a REST API with auth' }),
110
+ ['BACKEND_PROGRAMMER'],
111
+ { backend: 'FastAPI' },
112
+ );
113
+ expect(prompt).toContain('Build a REST API with auth');
114
+ });
115
+ });
116
+
117
+ describe('parseSkillPrompts', () => {
118
+ it('should parse valid JSON response', () => {
119
+ const response = JSON.stringify({
120
+ BACKEND_PROGRAMMER: 'You are the Backend Programmer for MyProject.',
121
+ DB_EXPERT: 'You are the DB Expert for MyProject.',
122
+ });
123
+ const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER', 'DB_EXPERT']);
124
+ expect(result.BACKEND_PROGRAMMER).toContain('Backend Programmer');
125
+ expect(result.DB_EXPERT).toContain('DB Expert');
126
+ });
127
+
128
+ it('should extract JSON from markdown code fences', () => {
129
+ const response = '```json\n{"BACKEND_PROGRAMMER": "You are the Backend Programmer."}\n```';
130
+ const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
131
+ expect(result.BACKEND_PROGRAMMER).toContain('Backend Programmer');
132
+ });
133
+
134
+ it('should return empty for malformed JSON', () => {
135
+ const result = parseSkillPrompts('not json at all', ['BACKEND_PROGRAMMER']);
136
+ expect(Object.keys(result)).toHaveLength(0);
137
+ });
138
+
139
+ it('should skip roles not in expectedRoles', () => {
140
+ const response = JSON.stringify({
141
+ BACKEND_PROGRAMMER: 'Valid prompt for backend.',
142
+ FRONTEND_PROGRAMMER: 'Should be ignored.',
143
+ });
144
+ const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
145
+ expect(result.BACKEND_PROGRAMMER).toBeDefined();
146
+ expect(result.FRONTEND_PROGRAMMER).toBeUndefined();
147
+ });
148
+
149
+ it('should skip prompts shorter than 10 chars', () => {
150
+ const response = JSON.stringify({
151
+ BACKEND_PROGRAMMER: 'Short',
152
+ });
153
+ const result = parseSkillPrompts(response, ['BACKEND_PROGRAMMER']);
154
+ expect(result.BACKEND_PROGRAMMER).toBeUndefined();
155
+ });
156
+ });
157
+
158
+ describe('renderSkillMarkdown', () => {
159
+ it('should produce valid YAML frontmatter format', () => {
160
+ const md = renderSkillMarkdown(
161
+ 'BACKEND_PROGRAMMER',
162
+ 'You are the Backend Programmer.',
163
+ ['follow_architecture', 'must_follow_master_plan'],
164
+ ['endpoints', 'services'],
165
+ ['ARCHITECT'],
166
+ );
167
+
168
+ expect(md).toMatch(/^---\n/);
169
+ expect(md).toContain('role: BACKEND_PROGRAMMER');
170
+ expect(md).toContain('version: 1.0-project');
171
+ expect(md).toContain(' - endpoints');
172
+ expect(md).toContain(' - services');
173
+ expect(md).toContain(' - follow_architecture');
174
+ expect(md).toContain(' - must_follow_master_plan');
175
+ expect(md).toContain('depends_on:');
176
+ expect(md).toContain(' - ARCHITECT');
177
+ expect(md).toContain('You are the Backend Programmer.');
178
+ });
179
+
180
+ it('should omit depends_on when empty', () => {
181
+ const md = renderSkillMarkdown(
182
+ 'DISPATCHER',
183
+ 'You are the Dispatcher.',
184
+ ['governance'],
185
+ ['phase_transition'],
186
+ [],
187
+ );
188
+ expect(md).not.toContain('depends_on:');
189
+ });
190
+ });
191
+
192
+ describe('writeGenerationMarker', () => {
193
+ it('should write valid JSON marker file', () => {
194
+ const marker = {
195
+ timestamp: '2026-02-22T14:30:00Z',
196
+ pipelineVersion: '1.0',
197
+ activeRoles: ['DISPATCHER', 'BACKEND_PROGRAMMER'],
198
+ techStack: { backend: 'FastAPI' },
199
+ aiGenerated: true,
200
+ };
201
+ writeGenerationMarker(SKILLS_DIR, marker);
202
+
203
+ const markerPath = join(SKILLS_DIR, '.popeye-skills-generated.json');
204
+ expect(existsSync(markerPath)).toBe(true);
205
+
206
+ const content = JSON.parse(readFileSync(markerPath, 'utf-8'));
207
+ expect(content.pipelineVersion).toBe('1.0');
208
+ expect(content.aiGenerated).toBe(true);
209
+ expect(content.activeRoles).toContain('BACKEND_PROGRAMMER');
210
+ expect(content.techStack.backend).toBe('FastAPI');
211
+ });
212
+ });
213
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Role map tests — role selection, tech stack inference, template constraints.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ getActiveRoles,
8
+ inferTechStack,
9
+ getTemplateConstraints,
10
+ SUPPORT_ROLES,
11
+ IMPLEMENTATION_ROLES,
12
+ } from '../../../src/pipeline/skills/role-map.js';
13
+ import type { RepoSnapshot } from '../../../src/pipeline/types.js';
14
+ import type { OutputLanguage } from '../../../src/types/project.js';
15
+
16
+ function makeSnapshot(overrides: Partial<RepoSnapshot> = {}): RepoSnapshot {
17
+ return {
18
+ snapshot_id: 'test-snap',
19
+ timestamp: new Date().toISOString(),
20
+ tree_summary: '',
21
+ config_files: [],
22
+ languages_detected: [],
23
+ scripts: {},
24
+ env_files: [],
25
+ migrations_present: false,
26
+ ports_entrypoints: [],
27
+ total_files: 0,
28
+ total_lines: 0,
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ describe('role-map', () => {
34
+ describe('getActiveRoles', () => {
35
+ it('should include support roles for all languages', () => {
36
+ const languages: OutputLanguage[] = ['python', 'typescript', 'fullstack', 'website', 'all'];
37
+ for (const lang of languages) {
38
+ const roles = getActiveRoles(lang);
39
+ for (const support of SUPPORT_ROLES) {
40
+ expect(roles).toContain(support);
41
+ }
42
+ }
43
+ });
44
+
45
+ it('should include DB_EXPERT for python', () => {
46
+ const roles = getActiveRoles('python');
47
+ expect(roles).toContain('DB_EXPERT');
48
+ expect(roles).toContain('BACKEND_PROGRAMMER');
49
+ expect(roles).not.toContain('FRONTEND_PROGRAMMER');
50
+ });
51
+
52
+ it('should include FRONTEND_PROGRAMMER for typescript (not BACKEND_PROGRAMMER)', () => {
53
+ const roles = getActiveRoles('typescript');
54
+ expect(roles).toContain('FRONTEND_PROGRAMMER');
55
+ expect(roles).toContain('UI_UX_SPECIALIST');
56
+ expect(roles).not.toContain('BACKEND_PROGRAMMER');
57
+ expect(roles).not.toContain('DB_EXPERT');
58
+ });
59
+
60
+ it('should include both FE and BE for fullstack', () => {
61
+ const roles = getActiveRoles('fullstack');
62
+ expect(roles).toContain('DB_EXPERT');
63
+ expect(roles).toContain('BACKEND_PROGRAMMER');
64
+ expect(roles).toContain('FRONTEND_PROGRAMMER');
65
+ expect(roles).toContain('UI_UX_SPECIALIST');
66
+ expect(roles).not.toContain('WEBSITE_PROGRAMMER');
67
+ });
68
+
69
+ it('should include website and marketing roles for website', () => {
70
+ const roles = getActiveRoles('website');
71
+ expect(roles).toContain('WEBSITE_PROGRAMMER');
72
+ expect(roles).toContain('UI_UX_SPECIALIST');
73
+ expect(roles).toContain('MARKETING_EXPERT');
74
+ expect(roles).toContain('SOCIAL_EXPERT');
75
+ expect(roles).not.toContain('BACKEND_PROGRAMMER');
76
+ });
77
+
78
+ it('should include all implementation roles for all', () => {
79
+ const roles = getActiveRoles('all');
80
+ for (const impl of IMPLEMENTATION_ROLES) {
81
+ expect(roles).toContain(impl);
82
+ }
83
+ });
84
+ });
85
+
86
+ describe('inferTechStack', () => {
87
+ it('should return language defaults when no signals', () => {
88
+ const ts = inferTechStack('python');
89
+ expect(ts.backend).toBe('FastAPI');
90
+ expect(ts.database).toBe('PostgreSQL');
91
+ expect(ts.orm).toBe('SQLAlchemy');
92
+ expect(ts.testing).toBe('Pytest');
93
+ expect(ts.language).toBe('Python 3.11+');
94
+ });
95
+
96
+ it('should detect FastAPI from snapshot key_fields', () => {
97
+ const snapshot = makeSnapshot({
98
+ config_files: [{
99
+ path: 'pyproject.toml',
100
+ type: 'toml',
101
+ content_hash: 'abc',
102
+ key_fields: { dependencies: ['fastapi', 'uvicorn'] },
103
+ }],
104
+ });
105
+ const ts = inferTechStack('python', snapshot);
106
+ expect(ts.backend).toBe('FastAPI');
107
+ });
108
+
109
+ it('should detect Django from snapshot key_fields', () => {
110
+ const snapshot = makeSnapshot({
111
+ config_files: [{
112
+ path: 'requirements.txt',
113
+ type: 'txt',
114
+ content_hash: 'abc',
115
+ key_fields: { packages: ['django', 'django-rest-framework'] },
116
+ }],
117
+ });
118
+ const ts = inferTechStack('python', snapshot);
119
+ expect(ts.backend).toBe('Django');
120
+ });
121
+
122
+ it('should detect framework from expanded spec when snapshot has no deps', () => {
123
+ const ts = inferTechStack('python', makeSnapshot(), 'Build a Django REST API');
124
+ expect(ts.backend).toBe('Django');
125
+ });
126
+
127
+ it('should prioritize snapshot over spec mentions', () => {
128
+ const snapshot = makeSnapshot({
129
+ config_files: [{
130
+ path: 'pyproject.toml',
131
+ type: 'toml',
132
+ content_hash: 'abc',
133
+ key_fields: { dependencies: ['fastapi'] },
134
+ }],
135
+ });
136
+ const ts = inferTechStack('python', snapshot, 'Build a Django API');
137
+ // Snapshot has fastapi, spec mentions Django — snapshot wins
138
+ expect(ts.backend).toBe('FastAPI');
139
+ });
140
+
141
+ it('should return typescript defaults', () => {
142
+ const ts = inferTechStack('typescript');
143
+ expect(ts.frontend).toBe('React + Vite');
144
+ expect(ts.testing).toBe('Vitest');
145
+ expect(ts.language).toBe('TypeScript 5.x');
146
+ });
147
+
148
+ it('should detect Next.js from snapshot', () => {
149
+ const snapshot = makeSnapshot({
150
+ config_files: [{
151
+ path: 'package.json',
152
+ type: 'json',
153
+ content_hash: 'abc',
154
+ key_fields: { dependencies: { next: '^14.0.0', react: '^18.0.0' } },
155
+ }],
156
+ });
157
+ const ts = inferTechStack('typescript', snapshot);
158
+ expect(ts.frontend).toBe('Next.js');
159
+ });
160
+ });
161
+
162
+ describe('getTemplateConstraints', () => {
163
+ it('should always include governance constraints', () => {
164
+ const constraints = getTemplateConstraints('BACKEND_PROGRAMMER', {});
165
+ expect(constraints).toContain('must_follow_master_plan');
166
+ expect(constraints).toContain('must_follow_architecture');
167
+ expect(constraints).toContain('conflicts_require_change_request');
168
+ });
169
+
170
+ it('should add FastAPI constraints for backend with FastAPI', () => {
171
+ const constraints = getTemplateConstraints('BACKEND_PROGRAMMER', {
172
+ backend: 'FastAPI',
173
+ testing: 'Pytest',
174
+ });
175
+ expect(constraints).toContain('fastapi_async_required');
176
+ expect(constraints).toContain('pydantic_validation');
177
+ expect(constraints).toContain('pytest_testing');
178
+ });
179
+
180
+ it('should add React constraints for frontend', () => {
181
+ const constraints = getTemplateConstraints('FRONTEND_PROGRAMMER', {
182
+ frontend: 'React + Vite',
183
+ testing: 'Vitest',
184
+ });
185
+ expect(constraints).toContain('react_component_pattern');
186
+ expect(constraints).toContain('component_testing');
187
+ });
188
+
189
+ it('should return only governance constraints for roles without tech constraints', () => {
190
+ const constraints = getTemplateConstraints('DISPATCHER', {});
191
+ expect(constraints).toEqual([
192
+ 'must_follow_master_plan',
193
+ 'must_follow_architecture',
194
+ 'conflicts_require_change_request',
195
+ ]);
196
+ });
197
+ });
198
+ });
@@ -141,7 +141,7 @@ describe('DEFAULT_CONSENSUS_CONFIG', () => {
141
141
  it('should have valid default values', () => {
142
142
  expect(DEFAULT_CONSENSUS_CONFIG.threshold).toBe(95);
143
143
  expect(DEFAULT_CONSENSUS_CONFIG.maxIterations).toBe(10);
144
- expect(DEFAULT_CONSENSUS_CONFIG.openaiModel).toBe('gpt-4o');
144
+ expect(DEFAULT_CONSENSUS_CONFIG.openaiModel).toBe('gpt-4.1');
145
145
  expect(DEFAULT_CONSENSUS_CONFIG.temperature).toBe(0.3);
146
146
  expect(DEFAULT_CONSENSUS_CONFIG.maxTokens).toBe(4096);
147
147
  });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Fix B tests — new projects use pipeline from start.
3
+ * Verifies that runWorkflow() bootstraps state before pipeline.
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+
8
+ // Mock the modules before importing
9
+ vi.mock('../../src/state/index.js', () => ({
10
+ loadProject: vi.fn(),
11
+ projectExists: vi.fn(),
12
+ getProgress: vi.fn(),
13
+ resetToPhase: vi.fn(),
14
+ deleteProject: vi.fn(),
15
+ verifyProjectCompletion: vi.fn(),
16
+ resetIncompleteProject: vi.fn(),
17
+ createProject: vi.fn(),
18
+ }));
19
+
20
+ vi.mock('../../src/pipeline/orchestrator.js', () => ({
21
+ runPipeline: vi.fn(),
22
+ resumePipeline: vi.fn(),
23
+ }));
24
+
25
+ vi.mock('../../src/workflow/plan-mode.js', () => ({
26
+ runPlanMode: vi.fn(),
27
+ resumePlanMode: vi.fn(),
28
+ }));
29
+
30
+ vi.mock('../../src/workflow/execution-mode.js', () => ({
31
+ runExecutionMode: vi.fn(),
32
+ resumeExecutionMode: vi.fn(),
33
+ executeSingleTask: vi.fn(),
34
+ }));
35
+
36
+ vi.mock('../../src/workflow/workflow-logger.js', () => ({
37
+ getWorkflowLogger: () => ({
38
+ stageStart: vi.fn(),
39
+ stageComplete: vi.fn(),
40
+ stageFailed: vi.fn(),
41
+ info: vi.fn(),
42
+ }),
43
+ }));
44
+
45
+ describe('Fix B: New projects use pipeline from start', () => {
46
+ beforeEach(() => {
47
+ vi.resetAllMocks();
48
+ // Default: pipeline mode enabled
49
+ delete process.env.POPEYE_LEGACY_WORKFLOW;
50
+ });
51
+
52
+ it('should create state and run pipeline when state does not exist', async () => {
53
+ const { loadProject, createProject } = await import('../../src/state/index.js');
54
+ const { runPipeline } = await import('../../src/pipeline/orchestrator.js');
55
+
56
+ const mockState = {
57
+ id: 'test-id',
58
+ name: 'test-project',
59
+ idea: 'Build something',
60
+ language: 'python' as const,
61
+ phase: 'plan' as const,
62
+ status: 'pending' as const,
63
+ milestones: [],
64
+ currentMilestone: null,
65
+ currentTask: null,
66
+ consensusHistory: [],
67
+ createdAt: new Date().toISOString(),
68
+ updatedAt: new Date().toISOString(),
69
+ };
70
+
71
+ // loadProject fails (no state.json) on first call, succeeds after create
72
+ vi.mocked(loadProject)
73
+ .mockRejectedValueOnce(new Error('No project found'))
74
+ .mockResolvedValue(mockState as any);
75
+
76
+ vi.mocked(createProject).mockResolvedValue(mockState as any);
77
+ vi.mocked(runPipeline).mockResolvedValue({
78
+ success: true,
79
+ finalPhase: 'DONE',
80
+ artifacts: [],
81
+ recoveryIterations: 0,
82
+ });
83
+
84
+ const { runWorkflow } = await import('../../src/workflow/index.js');
85
+
86
+ const result = await runWorkflow(
87
+ { idea: 'Build something', name: 'test-project', language: 'python', openaiModel: 'gpt-4o' },
88
+ { projectDir: '/tmp/test-project' },
89
+ );
90
+
91
+ expect(createProject).toHaveBeenCalledWith(
92
+ expect.objectContaining({ idea: 'Build something' }),
93
+ '/tmp/test-project',
94
+ );
95
+ expect(runPipeline).toHaveBeenCalledWith(
96
+ expect.objectContaining({
97
+ projectDir: '/tmp/test-project',
98
+ state: mockState,
99
+ }),
100
+ );
101
+ expect(result.success).toBe(true);
102
+ });
103
+
104
+ it('should use existing state when state already exists', async () => {
105
+ const { loadProject, createProject } = await import('../../src/state/index.js');
106
+ const { runPipeline } = await import('../../src/pipeline/orchestrator.js');
107
+
108
+ const existingState = {
109
+ id: 'existing-id',
110
+ name: 'existing-project',
111
+ phase: 'execution' as const,
112
+ };
113
+
114
+ vi.mocked(loadProject).mockResolvedValue(existingState as any);
115
+ vi.mocked(runPipeline).mockResolvedValue({
116
+ success: true,
117
+ finalPhase: 'DONE',
118
+ artifacts: [],
119
+ recoveryIterations: 0,
120
+ });
121
+
122
+ const { runWorkflow } = await import('../../src/workflow/index.js');
123
+
124
+ await runWorkflow(
125
+ { idea: 'Build something', name: 'existing-project', language: 'python', openaiModel: 'gpt-4o' },
126
+ { projectDir: '/tmp/existing-project' },
127
+ );
128
+
129
+ // Should NOT call createProject since state exists
130
+ expect(createProject).not.toHaveBeenCalled();
131
+ expect(runPipeline).toHaveBeenCalled();
132
+ });
133
+
134
+ it('should fall through to legacy workflow when POPEYE_LEGACY_WORKFLOW=1', async () => {
135
+ process.env.POPEYE_LEGACY_WORKFLOW = '1';
136
+
137
+ const { runPipeline } = await import('../../src/pipeline/orchestrator.js');
138
+ const { runPlanMode } = await import('../../src/workflow/plan-mode.js');
139
+ const { runExecutionMode } = await import('../../src/workflow/execution-mode.js');
140
+
141
+ vi.mocked(runPlanMode).mockResolvedValue({
142
+ success: true,
143
+ state: { phase: 'execution' } as any,
144
+ });
145
+ vi.mocked(runExecutionMode).mockResolvedValue({
146
+ success: true,
147
+ state: { phase: 'complete' } as any,
148
+ completedTasks: 5,
149
+ failedTasks: 0,
150
+ });
151
+
152
+ const { runWorkflow } = await import('../../src/workflow/index.js');
153
+
154
+ await runWorkflow(
155
+ { idea: 'Build something', name: 'test', language: 'python', openaiModel: 'gpt-4o' },
156
+ { projectDir: '/tmp/test' },
157
+ );
158
+
159
+ // Pipeline should NOT be called in legacy mode
160
+ expect(runPipeline).not.toHaveBeenCalled();
161
+ });
162
+ });