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,243 @@
1
+ /**
2
+ * Review Bridge tests — severity mapping, category mapping, CR routing,
3
+ * pipeline detection, finding conversion.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ mapSeverity,
9
+ mapCategory,
10
+ categoryToChangeType,
11
+ convertFinding,
12
+ isPipelineManaged,
13
+ extractPipelineState,
14
+ } from '../../../src/pipeline/bridges/review-bridge.js';
15
+ import { createDefaultPipelineState } from '../../../src/pipeline/types.js';
16
+ import type { AuditFinding as WorkflowFinding } from '../../../src/types/audit.js';
17
+ import type { ArtifactRef } from '../../../src/pipeline/types.js';
18
+ import type { ProjectState } from '../../../src/types/workflow.js';
19
+
20
+ // ─── Test Data ───────────────────────────────────────────
21
+
22
+ function makeSnapshotRef(): ArtifactRef {
23
+ return {
24
+ artifact_id: 'snap-001',
25
+ path: 'docs/snapshots/repo_snapshot_001.json',
26
+ sha256: 'abc123',
27
+ version: 1,
28
+ type: 'repo_snapshot',
29
+ };
30
+ }
31
+
32
+ function makeWorkflowFinding(overrides: Partial<WorkflowFinding> = {}): WorkflowFinding {
33
+ return {
34
+ id: 'finding-1',
35
+ category: 'integration-wiring',
36
+ severity: 'critical',
37
+ title: 'API endpoint mismatch',
38
+ description: 'Frontend calls /api/users but backend serves /api/v1/users',
39
+ evidence: [{ file: 'src/api.ts', line: 42, snippet: 'fetch("/api/users")' }],
40
+ recommendation: 'Update frontend API base URL',
41
+ autoFixable: true,
42
+ ...overrides,
43
+ };
44
+ }
45
+
46
+ function makeProjectState(withPipeline = false): ProjectState {
47
+ const base: ProjectState = {
48
+ id: 'test-id',
49
+ name: 'test-project',
50
+ idea: 'Build something',
51
+ language: 'python',
52
+ phase: 'execution',
53
+ status: 'in-progress',
54
+ milestones: [],
55
+ currentMilestone: null,
56
+ currentTask: null,
57
+ consensusHistory: [],
58
+ createdAt: new Date().toISOString(),
59
+ updatedAt: new Date().toISOString(),
60
+ } as ProjectState;
61
+
62
+ if (withPipeline) {
63
+ (base as unknown as { pipeline: unknown }).pipeline = createDefaultPipelineState();
64
+ }
65
+
66
+ return base;
67
+ }
68
+
69
+ // ─── Tests ───────────────────────────────────────────────
70
+
71
+ describe('Review Bridge', () => {
72
+ describe('severity mapping', () => {
73
+ it('should map critical to P0', () => {
74
+ expect(mapSeverity('critical')).toBe('P0');
75
+ });
76
+
77
+ it('should map major to P1', () => {
78
+ expect(mapSeverity('major')).toBe('P1');
79
+ });
80
+
81
+ it('should map minor to P2', () => {
82
+ expect(mapSeverity('minor')).toBe('P2');
83
+ });
84
+
85
+ it('should map info to P3', () => {
86
+ expect(mapSeverity('info')).toBe('P3');
87
+ });
88
+ });
89
+
90
+ describe('category mapping', () => {
91
+ it('should map integration-wiring to integration', () => {
92
+ expect(mapCategory('integration-wiring')).toBe('integration');
93
+ });
94
+
95
+ it('should map feature-completeness to integration', () => {
96
+ expect(mapCategory('feature-completeness')).toBe('integration');
97
+ });
98
+
99
+ it('should map test-coverage to tests', () => {
100
+ expect(mapCategory('test-coverage')).toBe('tests');
101
+ });
102
+
103
+ it('should map config-deployment to config', () => {
104
+ expect(mapCategory('config-deployment')).toBe('config');
105
+ });
106
+
107
+ it('should map security to security', () => {
108
+ expect(mapCategory('security')).toBe('security');
109
+ });
110
+
111
+ it('should map dependency-sanity to deployment', () => {
112
+ expect(mapCategory('dependency-sanity')).toBe('deployment');
113
+ });
114
+
115
+ it('should map consistency to schema', () => {
116
+ expect(mapCategory('consistency')).toBe('schema');
117
+ });
118
+
119
+ it('should map documentation to deployment', () => {
120
+ expect(mapCategory('documentation')).toBe('deployment');
121
+ });
122
+ });
123
+
124
+ describe('CR change type routing', () => {
125
+ it('should route integration to architecture CR', () => {
126
+ expect(categoryToChangeType('integration')).toBe('architecture');
127
+ });
128
+
129
+ it('should route schema to architecture CR', () => {
130
+ expect(categoryToChangeType('schema')).toBe('architecture');
131
+ });
132
+
133
+ it('should route security to requirement CR', () => {
134
+ expect(categoryToChangeType('security')).toBe('requirement');
135
+ });
136
+
137
+ it('should route tests to config CR', () => {
138
+ expect(categoryToChangeType('tests')).toBe('config');
139
+ });
140
+
141
+ it('should route config to config CR', () => {
142
+ expect(categoryToChangeType('config')).toBe('config');
143
+ });
144
+
145
+ it('should route deployment to config CR', () => {
146
+ expect(categoryToChangeType('deployment')).toBe('config');
147
+ });
148
+ });
149
+
150
+ describe('finding conversion', () => {
151
+ it('should convert a critical workflow finding to P0 pipeline finding', () => {
152
+ const wf = makeWorkflowFinding({ severity: 'critical', category: 'integration-wiring' });
153
+ const ref = makeSnapshotRef();
154
+
155
+ const pf = convertFinding(wf, ref);
156
+
157
+ expect(pf.id).toBe('finding-1');
158
+ expect(pf.severity).toBe('P0');
159
+ expect(pf.category).toBe('integration');
160
+ expect(pf.blocking).toBe(true);
161
+ expect(pf.description).toContain('API endpoint mismatch');
162
+ expect(pf.file_path).toBe('src/api.ts');
163
+ expect(pf.line_number).toBe(42);
164
+ expect(pf.evidence).toEqual([ref]);
165
+ expect(pf.suggested_owner).toBe('AUDITOR');
166
+ });
167
+
168
+ it('should convert an info finding to P3 non-blocking', () => {
169
+ const wf = makeWorkflowFinding({ severity: 'info', category: 'documentation' });
170
+ const ref = makeSnapshotRef();
171
+
172
+ const pf = convertFinding(wf, ref);
173
+
174
+ expect(pf.severity).toBe('P3');
175
+ expect(pf.blocking).toBe(false);
176
+ });
177
+
178
+ it('should mark P0 and P1 as blocking, P2 and P3 as non-blocking', () => {
179
+ const ref = makeSnapshotRef();
180
+
181
+ expect(convertFinding(makeWorkflowFinding({ severity: 'critical' }), ref).blocking).toBe(true);
182
+ expect(convertFinding(makeWorkflowFinding({ severity: 'major' }), ref).blocking).toBe(true);
183
+ expect(convertFinding(makeWorkflowFinding({ severity: 'minor' }), ref).blocking).toBe(false);
184
+ expect(convertFinding(makeWorkflowFinding({ severity: 'info' }), ref).blocking).toBe(false);
185
+ });
186
+
187
+ it('should handle finding with no evidence', () => {
188
+ const wf = makeWorkflowFinding({ evidence: [] });
189
+ const ref = makeSnapshotRef();
190
+
191
+ const pf = convertFinding(wf, ref);
192
+
193
+ expect(pf.file_path).toBeUndefined();
194
+ expect(pf.line_number).toBeUndefined();
195
+ });
196
+ });
197
+
198
+ describe('pipeline detection', () => {
199
+ it('should detect pipeline-managed state', () => {
200
+ const state = makeProjectState(true);
201
+ expect(isPipelineManaged(state)).toBe(true);
202
+ });
203
+
204
+ it('should detect non-pipeline state', () => {
205
+ const state = makeProjectState(false);
206
+ expect(isPipelineManaged(state)).toBe(false);
207
+ });
208
+
209
+ it('should extract pipeline state when present', () => {
210
+ const state = makeProjectState(true);
211
+ const pipeline = extractPipelineState(state);
212
+
213
+ expect(pipeline).toBeDefined();
214
+ expect(pipeline!.pipelinePhase).toBe('INTAKE');
215
+ });
216
+
217
+ it('should return undefined when no pipeline state', () => {
218
+ const state = makeProjectState(false);
219
+ expect(extractPipelineState(state)).toBeUndefined();
220
+ });
221
+ });
222
+
223
+ describe('CR routing determinism', () => {
224
+ it('should route integration findings to CONSENSUS_ARCHITECTURE', () => {
225
+ // Integration → architecture CR → CONSENSUS_ARCHITECTURE (via change-request.ts routing)
226
+ const changeType = categoryToChangeType('integration');
227
+ expect(changeType).toBe('architecture');
228
+ // architecture routes to CONSENSUS_ARCHITECTURE per CHANGE_TYPE_ROUTING
229
+ });
230
+
231
+ it('should route security findings to CONSENSUS_MASTER_PLAN', () => {
232
+ const changeType = categoryToChangeType('security');
233
+ expect(changeType).toBe('requirement');
234
+ // requirement routes to CONSENSUS_MASTER_PLAN per CHANGE_TYPE_ROUTING
235
+ });
236
+
237
+ it('should route test findings to QA_VALIDATION', () => {
238
+ const changeType = categoryToChangeType('tests');
239
+ expect(changeType).toBe('config');
240
+ // config routes to QA_VALIDATION per CHANGE_TYPE_ROUTING
241
+ });
242
+ });
243
+ });
@@ -95,8 +95,9 @@ describe('Migration', () => {
95
95
 
96
96
  expect(pipeline.activeRoles).toContain('DISPATCHER');
97
97
  expect(pipeline.activeRoles).toContain('ARCHITECT');
98
- expect(pipeline.activeRoles).toContain('BACKEND_PROGRAMMER');
99
- expect(pipeline.activeRoles).not.toContain('FRONTEND_PROGRAMMER');
98
+ expect(pipeline.activeRoles).toContain('FRONTEND_PROGRAMMER');
99
+ expect(pipeline.activeRoles).toContain('UI_UX_SPECIALIST');
100
+ expect(pipeline.activeRoles).not.toContain('BACKEND_PROGRAMMER');
100
101
  });
101
102
 
102
103
  it('should derive roles for fullstack project', () => {
@@ -106,8 +107,8 @@ describe('Migration', () => {
106
107
  expect(pipeline.activeRoles).toContain('DB_EXPERT');
107
108
  expect(pipeline.activeRoles).toContain('BACKEND_PROGRAMMER');
108
109
  expect(pipeline.activeRoles).toContain('FRONTEND_PROGRAMMER');
109
- expect(pipeline.activeRoles).toContain('WEBSITE_PROGRAMMER');
110
110
  expect(pipeline.activeRoles).toContain('UI_UX_SPECIALIST');
111
+ expect(pipeline.activeRoles).not.toContain('WEBSITE_PROGRAMMER');
111
112
  });
112
113
 
113
114
  it('should derive roles for website project', () => {
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Fix A tests — sessionGuidance threading through pipeline.
3
+ * Verifies additionalContext persists in PipelineState and reaches phases.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ createDefaultPipelineState,
9
+ PipelineStateSchema,
10
+ ArtifactTypeSchema,
11
+ } from '../../src/pipeline/types.js';
12
+
13
+ describe('Fix A: sessionGuidance threading', () => {
14
+ describe('PipelineState.sessionGuidance', () => {
15
+ it('should accept sessionGuidance in pipeline state', () => {
16
+ const state = createDefaultPipelineState();
17
+ state.sessionGuidance = 'Upgrade from v1 to v2: preserve API backwards compat';
18
+
19
+ const result = PipelineStateSchema.safeParse(state);
20
+ expect(result.success).toBe(true);
21
+ expect(result.data?.sessionGuidance).toBe(
22
+ 'Upgrade from v1 to v2: preserve API backwards compat',
23
+ );
24
+ });
25
+
26
+ it('should allow omitting sessionGuidance (backward compat)', () => {
27
+ const state = createDefaultPipelineState();
28
+ // No sessionGuidance set
29
+
30
+ const result = PipelineStateSchema.safeParse(state);
31
+ expect(result.success).toBe(true);
32
+ expect(result.data?.sessionGuidance).toBeUndefined();
33
+ });
34
+
35
+ it('should allow empty string for sessionGuidance', () => {
36
+ const state = createDefaultPipelineState();
37
+ state.sessionGuidance = '';
38
+
39
+ const result = PipelineStateSchema.safeParse(state);
40
+ expect(result.success).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe('additional_context artifact type', () => {
45
+ it('should accept additional_context as valid artifact type', () => {
46
+ const result = ArtifactTypeSchema.safeParse('additional_context');
47
+ expect(result.success).toBe(true);
48
+ });
49
+
50
+ it('should still accept all existing artifact types', () => {
51
+ const existingTypes = [
52
+ 'master_plan', 'architecture', 'role_plan', 'consensus',
53
+ 'arbitration', 'audit_report', 'rca_report', 'production_readiness',
54
+ 'change_request',
55
+ ];
56
+
57
+ for (const type of existingTypes) {
58
+ expect(ArtifactTypeSchema.safeParse(type).success).toBe(true);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe('PipelineOptions additionalContext -> sessionGuidance', () => {
64
+ it('should store additionalContext in pipeline state when not already set', () => {
65
+ const pipeline = createDefaultPipelineState();
66
+ const additionalContext = 'Focus on mobile-first responsive design';
67
+
68
+ // Simulates orchestrator logic
69
+ if (additionalContext && !pipeline.sessionGuidance) {
70
+ pipeline.sessionGuidance = additionalContext;
71
+ }
72
+
73
+ expect(pipeline.sessionGuidance).toBe('Focus on mobile-first responsive design');
74
+ });
75
+
76
+ it('should not overwrite existing sessionGuidance on resume', () => {
77
+ const pipeline = createDefaultPipelineState();
78
+ pipeline.sessionGuidance = 'Original guidance from first run';
79
+ const additionalContext = 'New guidance on resume';
80
+
81
+ // Simulates orchestrator logic
82
+ if (additionalContext && !pipeline.sessionGuidance) {
83
+ pipeline.sessionGuidance = additionalContext;
84
+ }
85
+
86
+ expect(pipeline.sessionGuidance).toBe('Original guidance from first run');
87
+ });
88
+
89
+ it('should leave sessionGuidance undefined when no additionalContext', () => {
90
+ const pipeline = createDefaultPipelineState();
91
+ const additionalContext: string | undefined = undefined;
92
+
93
+ if (additionalContext && !pipeline.sessionGuidance) {
94
+ pipeline.sessionGuidance = additionalContext;
95
+ }
96
+
97
+ expect(pipeline.sessionGuidance).toBeUndefined();
98
+ });
99
+ });
100
+
101
+ describe('INTAKE phase guidance injection', () => {
102
+ it('should prepend guidance to plan input when provided', () => {
103
+ const guidance = 'Upgrade context: migrate from Express to Fastify';
104
+ const expandedIdea = 'Build a REST API with user authentication';
105
+
106
+ const planInput = guidance
107
+ ? `${guidance}\n\n---\n\n${expandedIdea}`
108
+ : expandedIdea;
109
+
110
+ expect(planInput).toContain(guidance);
111
+ expect(planInput).toContain('---');
112
+ expect(planInput).toContain(expandedIdea);
113
+ expect(planInput.indexOf(guidance)).toBeLessThan(planInput.indexOf(expandedIdea));
114
+ });
115
+
116
+ it('should pass through expandedIdea unchanged when no guidance', () => {
117
+ const guidance = '';
118
+ const expandedIdea = 'Build a REST API with user authentication';
119
+
120
+ const planInput = guidance
121
+ ? `${guidance}\n\n---\n\n${expandedIdea}`
122
+ : expandedIdea;
123
+
124
+ expect(planInput).toBe(expandedIdea);
125
+ });
126
+ });
127
+
128
+ describe('IMPLEMENTATION phase guidance injection', () => {
129
+ it('should merge role prompt with guidance', () => {
130
+ const combinedRolePrompt = '## BACKEND_PROGRAMMER\nScope: API endpoints';
131
+ const guidance = 'Preserve backwards compatibility with v1 API';
132
+
133
+ const systemPrompt = [combinedRolePrompt, guidance].filter(Boolean).join('\n\n') || undefined;
134
+
135
+ expect(systemPrompt).toContain(combinedRolePrompt);
136
+ expect(systemPrompt).toContain(guidance);
137
+ });
138
+
139
+ it('should use only role prompt when no guidance', () => {
140
+ const combinedRolePrompt = '## BACKEND_PROGRAMMER\nScope: API endpoints';
141
+ const guidance: string | undefined = undefined;
142
+
143
+ const systemPrompt = [combinedRolePrompt, guidance].filter(Boolean).join('\n\n') || undefined;
144
+
145
+ expect(systemPrompt).toBe(combinedRolePrompt);
146
+ });
147
+
148
+ it('should use only guidance when no role prompt', () => {
149
+ const combinedRolePrompt = '';
150
+ const guidance = 'Focus on performance';
151
+
152
+ const systemPrompt = [combinedRolePrompt, guidance].filter(Boolean).join('\n\n') || undefined;
153
+
154
+ expect(systemPrompt).toBe(guidance);
155
+ });
156
+
157
+ it('should return undefined when neither role prompt nor guidance', () => {
158
+ const combinedRolePrompt = '';
159
+ const guidance: string | undefined = undefined;
160
+
161
+ const systemPrompt = [combinedRolePrompt, guidance].filter(Boolean).join('\n\n') || undefined;
162
+
163
+ expect(systemPrompt).toBeUndefined();
164
+ });
165
+ });
166
+
167
+ describe('RECOVERY_LOOP guidance in RCA prompt', () => {
168
+ it('should include user guidance in RCA prompt when available', () => {
169
+ const debuggerPrompt = 'You are a debugger agent...';
170
+ const guidance = 'User wants API backwards compat preserved';
171
+ const failureEvidence = 'Failed phase: QA_VALIDATION';
172
+
173
+ const rcaPrompt = [
174
+ debuggerPrompt,
175
+ '',
176
+ ...(guidance ? ['## User Guidance', guidance, ''] : []),
177
+ '## Failure Evidence',
178
+ failureEvidence,
179
+ ].join('\n');
180
+
181
+ expect(rcaPrompt).toContain('## User Guidance');
182
+ expect(rcaPrompt).toContain(guidance);
183
+ expect(rcaPrompt.indexOf('User Guidance')).toBeLessThan(
184
+ rcaPrompt.indexOf('Failure Evidence'),
185
+ );
186
+ });
187
+
188
+ it('should omit User Guidance section when no guidance', () => {
189
+ const debuggerPrompt = 'You are a debugger agent...';
190
+ const guidance: string | undefined = undefined;
191
+ const failureEvidence = 'Failed phase: QA_VALIDATION';
192
+
193
+ const rcaPrompt = [
194
+ debuggerPrompt,
195
+ '',
196
+ ...(guidance ? ['## User Guidance', guidance, ''] : []),
197
+ '## Failure Evidence',
198
+ failureEvidence,
199
+ ].join('\n');
200
+
201
+ expect(rcaPrompt).not.toContain('## User Guidance');
202
+ expect(rcaPrompt).toContain('## Failure Evidence');
203
+ });
204
+ });
205
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Constitution generator tests — deterministic template generation, 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
+ generateConstitution,
10
+ shouldSkipConstitution,
11
+ getTechStackSection,
12
+ getArchitectureRules,
13
+ getCodeQualityRules,
14
+ getConstraintsSection,
15
+ } from '../../../src/pipeline/skills/constitution-generator.js';
16
+ import type { ConstitutionContext } from '../../../src/pipeline/skills/types.js';
17
+
18
+ const TEST_DIR = join(process.cwd(), '.test-constitution-gen');
19
+ const SKILLS_DIR = join(TEST_DIR, 'skills');
20
+
21
+ function makeContext(overrides: Partial<ConstitutionContext> = {}): ConstitutionContext {
22
+ return {
23
+ language: 'python',
24
+ projectName: 'TestProject',
25
+ techStack: {
26
+ language: 'Python 3.11+',
27
+ backend: 'FastAPI',
28
+ database: 'PostgreSQL',
29
+ orm: 'SQLAlchemy',
30
+ testing: 'Pytest',
31
+ },
32
+ expandedSpec: 'Build a REST API',
33
+ skillsDir: SKILLS_DIR,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ describe('constitution-generator', () => {
39
+ beforeEach(() => {
40
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
41
+ mkdirSync(SKILLS_DIR, { recursive: true });
42
+ });
43
+
44
+ afterEach(() => {
45
+ if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
46
+ });
47
+
48
+ describe('shouldSkipConstitution', () => {
49
+ it('should return false when constitution does not exist', () => {
50
+ expect(shouldSkipConstitution(SKILLS_DIR)).toBe(false);
51
+ });
52
+
53
+ it('should return true when constitution already exists', () => {
54
+ writeFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'existing');
55
+ expect(shouldSkipConstitution(SKILLS_DIR)).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe('generateConstitution', () => {
60
+ it('should create POPEYE_CONSTITUTION.md', () => {
61
+ generateConstitution(makeContext());
62
+ const path = join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md');
63
+ expect(existsSync(path)).toBe(true);
64
+ });
65
+
66
+ it('should include project name in header', () => {
67
+ generateConstitution(makeContext());
68
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
69
+ expect(content).toContain('# Project Constitution: TestProject');
70
+ });
71
+
72
+ it('should include tech stack section', () => {
73
+ generateConstitution(makeContext());
74
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
75
+ expect(content).toContain('## Tech Stack');
76
+ expect(content).toContain('FastAPI');
77
+ expect(content).toContain('PostgreSQL');
78
+ expect(content).toContain('SQLAlchemy');
79
+ });
80
+
81
+ it('should include governance rules', () => {
82
+ generateConstitution(makeContext());
83
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
84
+ expect(content).toContain('## Governance Rules');
85
+ expect(content).toContain('Consensus threshold: 0.95');
86
+ expect(content).toContain('immutable once stored');
87
+ expect(content).toContain('No placeholder content');
88
+ });
89
+
90
+ it('should include immutability notice', () => {
91
+ generateConstitution(makeContext());
92
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
93
+ expect(content).toContain('## Immutability');
94
+ expect(content).toContain('MUST NOT be modified during pipeline execution');
95
+ });
96
+
97
+ it('should not overwrite existing constitution', () => {
98
+ writeFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'hand-written content');
99
+ generateConstitution(makeContext());
100
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
101
+ expect(content).toBe('hand-written content');
102
+ });
103
+
104
+ it('should include session guidance when provided', () => {
105
+ generateConstitution(makeContext({ sessionGuidance: 'Focus on security' }));
106
+ const content = readFileSync(join(SKILLS_DIR, 'POPEYE_CONSTITUTION.md'), 'utf-8');
107
+ expect(content).toContain('Focus on security');
108
+ });
109
+
110
+ it('should create skills dir if it does not exist', () => {
111
+ rmSync(SKILLS_DIR, { recursive: true });
112
+ expect(existsSync(SKILLS_DIR)).toBe(false);
113
+ generateConstitution(makeContext());
114
+ expect(existsSync(SKILLS_DIR)).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe('getTechStackSection', () => {
119
+ it('should list all available tech stack fields', () => {
120
+ const section = getTechStackSection({
121
+ language: 'Python 3.11+',
122
+ backend: 'FastAPI',
123
+ database: 'PostgreSQL',
124
+ orm: 'SQLAlchemy',
125
+ testing: 'Pytest',
126
+ });
127
+ expect(section).toContain('- Language: Python 3.11+');
128
+ expect(section).toContain('- Framework: FastAPI');
129
+ expect(section).toContain('- Database: PostgreSQL');
130
+ expect(section).toContain('- ORM: SQLAlchemy');
131
+ expect(section).toContain('- Testing: Pytest');
132
+ });
133
+
134
+ it('should skip undefined fields', () => {
135
+ const section = getTechStackSection({ language: 'Python 3.11+' });
136
+ expect(section).toContain('- Language: Python 3.11+');
137
+ expect(section).not.toContain('Framework');
138
+ expect(section).not.toContain('Database');
139
+ });
140
+ });
141
+
142
+ describe('getArchitectureRules', () => {
143
+ it('should include FastAPI async rule for FastAPI projects', () => {
144
+ const rules = getArchitectureRules({ backend: 'FastAPI' });
145
+ expect(rules).toContain('async/await');
146
+ });
147
+
148
+ it('should include SQLAlchemy rule for SQLAlchemy projects', () => {
149
+ const rules = getArchitectureRules({ orm: 'SQLAlchemy' });
150
+ expect(rules).toContain('SQLAlchemy ORM');
151
+ });
152
+
153
+ it('should include Python-specific rules', () => {
154
+ const rules = getArchitectureRules({ language: 'Python 3.11+' });
155
+ expect(rules).toContain('PEP8');
156
+ expect(rules).toContain('python-dotenv');
157
+ });
158
+
159
+ it('should include TypeScript-specific rules', () => {
160
+ const rules = getArchitectureRules({ language: 'TypeScript 5.x' });
161
+ expect(rules).toContain('strict mode');
162
+ });
163
+
164
+ it('should provide generic rules when no tech matches', () => {
165
+ const rules = getArchitectureRules({});
166
+ expect(rules).toContain('Environment variables');
167
+ });
168
+ });
169
+
170
+ describe('getCodeQualityRules', () => {
171
+ it('should include file size limit', () => {
172
+ const rules = getCodeQualityRules();
173
+ expect(rules).toContain('500 lines');
174
+ });
175
+
176
+ it('should include test requirements', () => {
177
+ const rules = getCodeQualityRules();
178
+ expect(rules).toContain('Unit tests');
179
+ });
180
+ });
181
+
182
+ describe('getConstraintsSection', () => {
183
+ it('should include Python constraints for python language', () => {
184
+ const section = getConstraintsSection('python');
185
+ expect(section).toContain('Python 3.11+');
186
+ expect(section).toContain('venv');
187
+ });
188
+
189
+ it('should include TypeScript constraints for typescript', () => {
190
+ const section = getConstraintsSection('typescript');
191
+ expect(section).toContain('Node.js 18+');
192
+ expect(section).toContain('ESM');
193
+ });
194
+
195
+ it('should include session guidance when provided', () => {
196
+ const section = getConstraintsSection('python', 'Focus on security');
197
+ expect(section).toContain('Session-Specific Guidance');
198
+ expect(section).toContain('Focus on security');
199
+ });
200
+ });
201
+ });