popeye-cli 1.10.0 → 2.0.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 (253) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CONTRIBUTING.md +15 -1
  3. package/README.md +57 -0
  4. package/dist/pipeline/artifact-manager.d.ts +47 -0
  5. package/dist/pipeline/artifact-manager.d.ts.map +1 -0
  6. package/dist/pipeline/artifact-manager.js +251 -0
  7. package/dist/pipeline/artifact-manager.js.map +1 -0
  8. package/dist/pipeline/artifact-validators.d.ts +29 -0
  9. package/dist/pipeline/artifact-validators.d.ts.map +1 -0
  10. package/dist/pipeline/artifact-validators.js +173 -0
  11. package/dist/pipeline/artifact-validators.js.map +1 -0
  12. package/dist/pipeline/change-request.d.ts +47 -0
  13. package/dist/pipeline/change-request.d.ts.map +1 -0
  14. package/dist/pipeline/change-request.js +91 -0
  15. package/dist/pipeline/change-request.js.map +1 -0
  16. package/dist/pipeline/check-runner.d.ts +47 -0
  17. package/dist/pipeline/check-runner.d.ts.map +1 -0
  18. package/dist/pipeline/check-runner.js +417 -0
  19. package/dist/pipeline/check-runner.js.map +1 -0
  20. package/dist/pipeline/command-resolver.d.ts +9 -0
  21. package/dist/pipeline/command-resolver.d.ts.map +1 -0
  22. package/dist/pipeline/command-resolver.js +140 -0
  23. package/dist/pipeline/command-resolver.js.map +1 -0
  24. package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
  25. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
  26. package/dist/pipeline/consensus/consensus-runner.js +212 -0
  27. package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
  28. package/dist/pipeline/constitution.d.ts +45 -0
  29. package/dist/pipeline/constitution.d.ts.map +1 -0
  30. package/dist/pipeline/constitution.js +82 -0
  31. package/dist/pipeline/constitution.js.map +1 -0
  32. package/dist/pipeline/gate-engine.d.ts +55 -0
  33. package/dist/pipeline/gate-engine.d.ts.map +1 -0
  34. package/dist/pipeline/gate-engine.js +270 -0
  35. package/dist/pipeline/gate-engine.js.map +1 -0
  36. package/dist/pipeline/index.d.ts +26 -0
  37. package/dist/pipeline/index.d.ts.map +1 -0
  38. package/dist/pipeline/index.js +35 -0
  39. package/dist/pipeline/index.js.map +1 -0
  40. package/dist/pipeline/migration.d.ts +15 -0
  41. package/dist/pipeline/migration.d.ts.map +1 -0
  42. package/dist/pipeline/migration.js +76 -0
  43. package/dist/pipeline/migration.js.map +1 -0
  44. package/dist/pipeline/orchestrator.d.ts +28 -0
  45. package/dist/pipeline/orchestrator.d.ts.map +1 -0
  46. package/dist/pipeline/orchestrator.js +238 -0
  47. package/dist/pipeline/orchestrator.js.map +1 -0
  48. package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
  49. package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
  50. package/dist/pipeline/packets/audit-report-builder.js +32 -0
  51. package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
  52. package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
  53. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
  54. package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
  55. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
  56. package/dist/pipeline/packets/index.d.ts +12 -0
  57. package/dist/pipeline/packets/index.d.ts.map +1 -0
  58. package/dist/pipeline/packets/index.js +8 -0
  59. package/dist/pipeline/packets/index.js.map +1 -0
  60. package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
  61. package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
  62. package/dist/pipeline/packets/plan-packet-builder.js +27 -0
  63. package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
  64. package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
  65. package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
  66. package/dist/pipeline/packets/rca-packet-builder.js +22 -0
  67. package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
  68. package/dist/pipeline/phases/architecture.d.ts +7 -0
  69. package/dist/pipeline/phases/architecture.d.ts.map +1 -0
  70. package/dist/pipeline/phases/architecture.js +60 -0
  71. package/dist/pipeline/phases/architecture.js.map +1 -0
  72. package/dist/pipeline/phases/audit.d.ts +8 -0
  73. package/dist/pipeline/phases/audit.d.ts.map +1 -0
  74. package/dist/pipeline/phases/audit.js +144 -0
  75. package/dist/pipeline/phases/audit.js.map +1 -0
  76. package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
  77. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
  78. package/dist/pipeline/phases/consensus-architecture.js +84 -0
  79. package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
  80. package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
  81. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
  82. package/dist/pipeline/phases/consensus-master-plan.js +81 -0
  83. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
  84. package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
  85. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
  86. package/dist/pipeline/phases/consensus-role-plans.js +85 -0
  87. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
  88. package/dist/pipeline/phases/done.d.ts +7 -0
  89. package/dist/pipeline/phases/done.d.ts.map +1 -0
  90. package/dist/pipeline/phases/done.js +45 -0
  91. package/dist/pipeline/phases/done.js.map +1 -0
  92. package/dist/pipeline/phases/implementation.d.ts +8 -0
  93. package/dist/pipeline/phases/implementation.d.ts.map +1 -0
  94. package/dist/pipeline/phases/implementation.js +42 -0
  95. package/dist/pipeline/phases/implementation.js.map +1 -0
  96. package/dist/pipeline/phases/index.d.ts +20 -0
  97. package/dist/pipeline/phases/index.d.ts.map +1 -0
  98. package/dist/pipeline/phases/index.js +19 -0
  99. package/dist/pipeline/phases/index.js.map +1 -0
  100. package/dist/pipeline/phases/intake.d.ts +8 -0
  101. package/dist/pipeline/phases/intake.d.ts.map +1 -0
  102. package/dist/pipeline/phases/intake.js +40 -0
  103. package/dist/pipeline/phases/intake.js.map +1 -0
  104. package/dist/pipeline/phases/phase-context.d.ts +30 -0
  105. package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
  106. package/dist/pipeline/phases/phase-context.js +33 -0
  107. package/dist/pipeline/phases/phase-context.js.map +1 -0
  108. package/dist/pipeline/phases/production-gate.d.ts +8 -0
  109. package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
  110. package/dist/pipeline/phases/production-gate.js +84 -0
  111. package/dist/pipeline/phases/production-gate.js.map +1 -0
  112. package/dist/pipeline/phases/qa-validation.d.ts +7 -0
  113. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
  114. package/dist/pipeline/phases/qa-validation.js +50 -0
  115. package/dist/pipeline/phases/qa-validation.js.map +1 -0
  116. package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
  117. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
  118. package/dist/pipeline/phases/recovery-loop.js +91 -0
  119. package/dist/pipeline/phases/recovery-loop.js.map +1 -0
  120. package/dist/pipeline/phases/review.d.ts +8 -0
  121. package/dist/pipeline/phases/review.d.ts.map +1 -0
  122. package/dist/pipeline/phases/review.js +127 -0
  123. package/dist/pipeline/phases/review.js.map +1 -0
  124. package/dist/pipeline/phases/role-planning.d.ts +7 -0
  125. package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
  126. package/dist/pipeline/phases/role-planning.js +75 -0
  127. package/dist/pipeline/phases/role-planning.js.map +1 -0
  128. package/dist/pipeline/phases/stuck.d.ts +7 -0
  129. package/dist/pipeline/phases/stuck.d.ts.map +1 -0
  130. package/dist/pipeline/phases/stuck.js +51 -0
  131. package/dist/pipeline/phases/stuck.js.map +1 -0
  132. package/dist/pipeline/repo-snapshot.d.ts +24 -0
  133. package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
  134. package/dist/pipeline/repo-snapshot.js +343 -0
  135. package/dist/pipeline/repo-snapshot.js.map +1 -0
  136. package/dist/pipeline/role-execution-adapter.d.ts +59 -0
  137. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
  138. package/dist/pipeline/role-execution-adapter.js +159 -0
  139. package/dist/pipeline/role-execution-adapter.js.map +1 -0
  140. package/dist/pipeline/skill-loader.d.ts +34 -0
  141. package/dist/pipeline/skill-loader.d.ts.map +1 -0
  142. package/dist/pipeline/skill-loader.js +156 -0
  143. package/dist/pipeline/skill-loader.js.map +1 -0
  144. package/dist/pipeline/skills/defaults.d.ts +16 -0
  145. package/dist/pipeline/skills/defaults.d.ts.map +1 -0
  146. package/dist/pipeline/skills/defaults.js +189 -0
  147. package/dist/pipeline/skills/defaults.js.map +1 -0
  148. package/dist/pipeline/type-defs/artifacts.d.ts +202 -0
  149. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
  150. package/dist/pipeline/type-defs/artifacts.js +66 -0
  151. package/dist/pipeline/type-defs/artifacts.js.map +1 -0
  152. package/dist/pipeline/type-defs/audit.d.ts +256 -0
  153. package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
  154. package/dist/pipeline/type-defs/audit.js +54 -0
  155. package/dist/pipeline/type-defs/audit.js.map +1 -0
  156. package/dist/pipeline/type-defs/checks.d.ts +81 -0
  157. package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
  158. package/dist/pipeline/type-defs/checks.js +38 -0
  159. package/dist/pipeline/type-defs/checks.js.map +1 -0
  160. package/dist/pipeline/type-defs/enums.d.ts +43 -0
  161. package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
  162. package/dist/pipeline/type-defs/enums.js +55 -0
  163. package/dist/pipeline/type-defs/enums.js.map +1 -0
  164. package/dist/pipeline/type-defs/index.d.ts +12 -0
  165. package/dist/pipeline/type-defs/index.d.ts.map +1 -0
  166. package/dist/pipeline/type-defs/index.js +12 -0
  167. package/dist/pipeline/type-defs/index.js.map +1 -0
  168. package/dist/pipeline/type-defs/packets.d.ts +806 -0
  169. package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
  170. package/dist/pipeline/type-defs/packets.js +109 -0
  171. package/dist/pipeline/type-defs/packets.js.map +1 -0
  172. package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
  173. package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
  174. package/dist/pipeline/type-defs/snapshot.js +35 -0
  175. package/dist/pipeline/type-defs/snapshot.js.map +1 -0
  176. package/dist/pipeline/type-defs/state.d.ts +449 -0
  177. package/dist/pipeline/type-defs/state.d.ts.map +1 -0
  178. package/dist/pipeline/type-defs/state.js +88 -0
  179. package/dist/pipeline/type-defs/state.js.map +1 -0
  180. package/dist/pipeline/types.d.ts +16 -0
  181. package/dist/pipeline/types.d.ts.map +1 -0
  182. package/dist/pipeline/types.js +16 -0
  183. package/dist/pipeline/types.js.map +1 -0
  184. package/dist/types/audit.d.ts +6 -6
  185. package/dist/workflow/index.d.ts.map +1 -1
  186. package/dist/workflow/index.js +48 -0
  187. package/dist/workflow/index.js.map +1 -1
  188. package/package.json +1 -1
  189. package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
  190. package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
  191. package/src/pipeline/artifact-manager.ts +339 -0
  192. package/src/pipeline/artifact-validators.ts +224 -0
  193. package/src/pipeline/change-request.ts +119 -0
  194. package/src/pipeline/check-runner.ts +504 -0
  195. package/src/pipeline/command-resolver.ts +168 -0
  196. package/src/pipeline/consensus/consensus-runner.ts +317 -0
  197. package/src/pipeline/constitution.ts +109 -0
  198. package/src/pipeline/gate-engine.ts +347 -0
  199. package/src/pipeline/index.ts +82 -0
  200. package/src/pipeline/migration.ts +91 -0
  201. package/src/pipeline/orchestrator.ts +314 -0
  202. package/src/pipeline/packets/audit-report-builder.ts +47 -0
  203. package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
  204. package/src/pipeline/packets/index.ts +15 -0
  205. package/src/pipeline/packets/plan-packet-builder.ts +52 -0
  206. package/src/pipeline/packets/rca-packet-builder.ts +38 -0
  207. package/src/pipeline/phases/architecture.ts +73 -0
  208. package/src/pipeline/phases/audit.ts +193 -0
  209. package/src/pipeline/phases/consensus-architecture.ts +104 -0
  210. package/src/pipeline/phases/consensus-master-plan.ts +100 -0
  211. package/src/pipeline/phases/consensus-role-plans.ts +105 -0
  212. package/src/pipeline/phases/done.ts +68 -0
  213. package/src/pipeline/phases/implementation.ts +48 -0
  214. package/src/pipeline/phases/index.ts +21 -0
  215. package/src/pipeline/phases/intake.ts +54 -0
  216. package/src/pipeline/phases/phase-context.ts +86 -0
  217. package/src/pipeline/phases/production-gate.ts +113 -0
  218. package/src/pipeline/phases/qa-validation.ts +63 -0
  219. package/src/pipeline/phases/recovery-loop.ts +118 -0
  220. package/src/pipeline/phases/review.ts +149 -0
  221. package/src/pipeline/phases/role-planning.ts +92 -0
  222. package/src/pipeline/phases/stuck.ts +62 -0
  223. package/src/pipeline/repo-snapshot.ts +395 -0
  224. package/src/pipeline/role-execution-adapter.ts +238 -0
  225. package/src/pipeline/skill-loader.ts +192 -0
  226. package/src/pipeline/skills/defaults.ts +215 -0
  227. package/src/pipeline/type-defs/artifacts.ts +81 -0
  228. package/src/pipeline/type-defs/audit.ts +67 -0
  229. package/src/pipeline/type-defs/checks.ts +47 -0
  230. package/src/pipeline/type-defs/enums.ts +62 -0
  231. package/src/pipeline/type-defs/index.ts +12 -0
  232. package/src/pipeline/type-defs/packets.ts +131 -0
  233. package/src/pipeline/type-defs/snapshot.ts +55 -0
  234. package/src/pipeline/type-defs/state.ts +165 -0
  235. package/src/pipeline/types.ts +16 -0
  236. package/src/workflow/index.ts +48 -0
  237. package/tests/pipeline/artifact-manager.test.ts +183 -0
  238. package/tests/pipeline/artifact-validators.test.ts +207 -0
  239. package/tests/pipeline/change-request.test.ts +180 -0
  240. package/tests/pipeline/check-runner.test.ts +157 -0
  241. package/tests/pipeline/command-resolver.test.ts +159 -0
  242. package/tests/pipeline/consensus-runner.test.ts +206 -0
  243. package/tests/pipeline/consensus-scoring.test.ts +163 -0
  244. package/tests/pipeline/constitution.test.ts +122 -0
  245. package/tests/pipeline/gate-engine.test.ts +195 -0
  246. package/tests/pipeline/migration.test.ts +133 -0
  247. package/tests/pipeline/orchestrator.test.ts +614 -0
  248. package/tests/pipeline/packets/builders.test.ts +347 -0
  249. package/tests/pipeline/repo-snapshot.test.ts +189 -0
  250. package/tests/pipeline/role-execution-adapter.test.ts +299 -0
  251. package/tests/pipeline/skill-loader.test.ts +186 -0
  252. package/tests/pipeline/start-env-checks.test.ts +123 -0
  253. package/tests/pipeline/types.test.ts +156 -0
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Orchestrator tests — full loop with mocked phases, recovery/rewind/stuck.
3
+ * Tests the deterministic transition logic (P0-A) without real LLM calls.
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { createGateEngine } from '../../src/pipeline/gate-engine.js';
8
+ import { createDefaultPipelineState } from '../../src/pipeline/types.js';
9
+ import type { PipelinePhase, PipelineState, ArtifactEntry } from '../../src/pipeline/types.js';
10
+ import type { GateResult } from '../../src/pipeline/gate-engine.js';
11
+
12
+ // We test the orchestrator's core logic by simulating gate/phase behavior
13
+ // rather than importing runPipeline (which pulls in LLM deps)
14
+
15
+ function makeArtifact(type: string, phase: string): ArtifactEntry {
16
+ return {
17
+ id: `test-${type}-${phase}-${Date.now()}`,
18
+ type: type as ArtifactEntry['type'],
19
+ phase: phase as PipelinePhase,
20
+ version: 1,
21
+ path: `docs/${type}.md`,
22
+ sha256: 'abc123',
23
+ timestamp: new Date().toISOString(),
24
+ immutable: true,
25
+ content_type: 'markdown',
26
+ group_id: `group-${type}`,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Simulates the orchestrator's core transition logic without
32
+ * actually running phase handlers or importing LLM dependencies.
33
+ */
34
+ function simulateOrchestratorLoop(
35
+ startPhase: PipelinePhase,
36
+ pipeline: PipelineState,
37
+ phaseOutcomes: Map<PipelinePhase, boolean>, // phase -> gate pass?
38
+ maxIterations = 50,
39
+ ): { finalPhase: PipelinePhase; recoveryCount: number; phaseLog: PipelinePhase[] } {
40
+ const engine = createGateEngine();
41
+ let phase = startPhase;
42
+ let failedPhase: PipelinePhase | null = null;
43
+ const phaseLog: PipelinePhase[] = [phase];
44
+
45
+ for (let i = 0; i < maxIterations; i++) {
46
+ if (phase === 'DONE' || phase === 'STUCK') break;
47
+
48
+ const gatePass = phaseOutcomes.get(phase) ?? false;
49
+
50
+ if (gatePass) {
51
+ if (phase === 'RECOVERY_LOOP') {
52
+ // Recovery succeeded — go back to failed phase
53
+ phase = failedPhase ?? 'QA_VALIDATION';
54
+ failedPhase = null;
55
+ } else {
56
+ const gateResult: GateResult = {
57
+ phase,
58
+ pass: true,
59
+ blockers: [],
60
+ missingArtifacts: [],
61
+ failedChecks: [],
62
+ timestamp: new Date().toISOString(),
63
+ };
64
+ phase = engine.getNextPhase(phase, gateResult);
65
+ }
66
+ } else {
67
+ if (pipeline.recoveryCount >= pipeline.maxRecoveryIterations) {
68
+ phase = 'STUCK';
69
+ } else {
70
+ failedPhase = phase;
71
+ phase = 'RECOVERY_LOOP';
72
+ pipeline.recoveryCount++;
73
+ }
74
+ }
75
+
76
+ pipeline.pipelinePhase = phase;
77
+ phaseLog.push(phase);
78
+ }
79
+
80
+ return { finalPhase: phase, recoveryCount: pipeline.recoveryCount, phaseLog };
81
+ }
82
+
83
+ describe('Orchestrator Transition Logic', () => {
84
+ let pipeline: PipelineState;
85
+
86
+ beforeEach(() => {
87
+ pipeline = createDefaultPipelineState();
88
+ });
89
+
90
+ describe('happy path', () => {
91
+ it('should progress through all phases to DONE', () => {
92
+ // All phases pass
93
+ const outcomes = new Map<PipelinePhase, boolean>([
94
+ ['INTAKE', true],
95
+ ['CONSENSUS_MASTER_PLAN', true],
96
+ ['ARCHITECTURE', true],
97
+ ['CONSENSUS_ARCHITECTURE', true],
98
+ ['ROLE_PLANNING', true],
99
+ ['CONSENSUS_ROLE_PLANS', true],
100
+ ['IMPLEMENTATION', true],
101
+ ['QA_VALIDATION', true],
102
+ ['REVIEW', true],
103
+ ['AUDIT', true],
104
+ ['PRODUCTION_GATE', true],
105
+ ]);
106
+
107
+ const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
108
+ expect(result.finalPhase).toBe('DONE');
109
+ expect(result.recoveryCount).toBe(0);
110
+ });
111
+
112
+ it('should follow the correct phase sequence', () => {
113
+ const outcomes = new Map<PipelinePhase, boolean>([
114
+ ['INTAKE', true],
115
+ ['CONSENSUS_MASTER_PLAN', true],
116
+ ['ARCHITECTURE', true],
117
+ ['CONSENSUS_ARCHITECTURE', true],
118
+ ['ROLE_PLANNING', true],
119
+ ['CONSENSUS_ROLE_PLANS', true],
120
+ ['IMPLEMENTATION', true],
121
+ ['QA_VALIDATION', true],
122
+ ['REVIEW', true],
123
+ ['AUDIT', true],
124
+ ['PRODUCTION_GATE', true],
125
+ ]);
126
+
127
+ const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
128
+ expect(result.phaseLog).toEqual([
129
+ 'INTAKE',
130
+ 'CONSENSUS_MASTER_PLAN',
131
+ 'ARCHITECTURE',
132
+ 'CONSENSUS_ARCHITECTURE',
133
+ 'ROLE_PLANNING',
134
+ 'CONSENSUS_ROLE_PLANS',
135
+ 'IMPLEMENTATION',
136
+ 'QA_VALIDATION',
137
+ 'REVIEW',
138
+ 'AUDIT',
139
+ 'PRODUCTION_GATE',
140
+ 'DONE',
141
+ ]);
142
+ });
143
+ });
144
+
145
+ describe('recovery loop', () => {
146
+ it('should enter RECOVERY_LOOP on gate failure', () => {
147
+ // INTAKE passes, CONSENSUS fails first time, then passes
148
+ let consensusAttempt = 0;
149
+ const outcomes = new Map<PipelinePhase, boolean>([
150
+ ['INTAKE', true],
151
+ ['RECOVERY_LOOP', true],
152
+ ]);
153
+
154
+ // Override CONSENSUS to fail once then pass
155
+ const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
156
+ // INTAKE passes -> CONSENSUS (not in outcomes, defaults to false) -> RECOVERY -> CONSENSUS (still false)
157
+ // Eventually hits max recovery
158
+ expect(result.phaseLog).toContain('RECOVERY_LOOP');
159
+ });
160
+
161
+ it('should go to STUCK after max recovery iterations', () => {
162
+ pipeline.maxRecoveryIterations = 3;
163
+
164
+ // Only INTAKE passes, everything else fails
165
+ const outcomes = new Map<PipelinePhase, boolean>([
166
+ ['INTAKE', true],
167
+ ]);
168
+
169
+ const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
170
+ expect(result.finalPhase).toBe('STUCK');
171
+ expect(result.recoveryCount).toBe(3);
172
+ });
173
+
174
+ it('should return to failed phase after successful recovery', () => {
175
+ let qaAttempts = 0;
176
+
177
+ // Custom simulation: QA fails once, recovery succeeds, QA succeeds on retry
178
+ const engine = createGateEngine();
179
+ let phase: PipelinePhase = 'QA_VALIDATION';
180
+ let failedPhase: PipelinePhase | null = null;
181
+ const log: PipelinePhase[] = [phase];
182
+
183
+ // Pre-fill pipeline to reach QA
184
+ // Step 1: QA fails
185
+ failedPhase = 'QA_VALIDATION';
186
+ phase = 'RECOVERY_LOOP';
187
+ pipeline.recoveryCount++;
188
+ log.push(phase);
189
+
190
+ // Step 2: RECOVERY passes, returns to QA
191
+ phase = failedPhase;
192
+ failedPhase = null;
193
+ log.push(phase);
194
+
195
+ // Step 3: QA passes this time
196
+ const gr: GateResult = { phase, pass: true, blockers: [], missingArtifacts: [], failedChecks: [], timestamp: '' };
197
+ phase = engine.getNextPhase('QA_VALIDATION', gr);
198
+ log.push(phase);
199
+
200
+ expect(log).toEqual(['QA_VALIDATION', 'RECOVERY_LOOP', 'QA_VALIDATION', 'REVIEW']);
201
+ expect(pipeline.recoveryCount).toBe(1);
202
+ });
203
+ });
204
+
205
+ describe('resume from mid-pipeline', () => {
206
+ it('should resume from ARCHITECTURE', () => {
207
+ const outcomes = new Map<PipelinePhase, boolean>([
208
+ ['ARCHITECTURE', true],
209
+ ['CONSENSUS_ARCHITECTURE', true],
210
+ ['ROLE_PLANNING', true],
211
+ ['CONSENSUS_ROLE_PLANS', true],
212
+ ['IMPLEMENTATION', true],
213
+ ['QA_VALIDATION', true],
214
+ ['REVIEW', true],
215
+ ['AUDIT', true],
216
+ ['PRODUCTION_GATE', true],
217
+ ]);
218
+
219
+ const result = simulateOrchestratorLoop('ARCHITECTURE', pipeline, outcomes);
220
+ expect(result.finalPhase).toBe('DONE');
221
+ expect(result.phaseLog[0]).toBe('ARCHITECTURE');
222
+ });
223
+
224
+ it('should resume from IMPLEMENTATION', () => {
225
+ const outcomes = new Map<PipelinePhase, boolean>([
226
+ ['IMPLEMENTATION', true],
227
+ ['QA_VALIDATION', true],
228
+ ['REVIEW', true],
229
+ ['AUDIT', true],
230
+ ['PRODUCTION_GATE', true],
231
+ ]);
232
+
233
+ const result = simulateOrchestratorLoop('IMPLEMENTATION', pipeline, outcomes);
234
+ expect(result.finalPhase).toBe('DONE');
235
+ });
236
+ });
237
+
238
+ describe('gate engine integration', () => {
239
+ it('should evaluate INTAKE gate correctly', () => {
240
+ const engine = createGateEngine();
241
+
242
+ // Missing artifacts
243
+ const result1 = engine.evaluateGate('INTAKE', pipeline);
244
+ expect(result1.pass).toBe(false);
245
+
246
+ // Add required artifacts (including constitution for v1.1)
247
+ pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
248
+ pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
249
+ pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
250
+
251
+ const result2 = engine.evaluateGate('INTAKE', pipeline);
252
+ expect(result2.pass).toBe(true);
253
+ });
254
+
255
+ it('should fail gate when constitution is invalid (v1.1)', () => {
256
+ const engine = createGateEngine();
257
+
258
+ // Add all required artifacts
259
+ pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
260
+ pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
261
+ pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
262
+
263
+ // Gate should pass normally
264
+ const passResult = engine.evaluateGate('INTAKE', pipeline);
265
+ expect(passResult.pass).toBe(true);
266
+
267
+ // With constitutionValid=false, gate should fail
268
+ const failResult = engine.evaluateGate('INTAKE', pipeline, {
269
+ constitutionValid: false,
270
+ constitutionReason: 'Constitution has been modified since pipeline start',
271
+ });
272
+ expect(failResult.pass).toBe(false);
273
+ expect(failResult.blockers.some((b: string) => b.includes('Constitution'))).toBe(true);
274
+ });
275
+
276
+ it('should evaluate PRODUCTION_GATE with check results', () => {
277
+ const engine = createGateEngine();
278
+
279
+ // Add required artifacts (production_readiness + audit_report)
280
+ pipeline.artifacts.push(makeArtifact('production_readiness', 'PRODUCTION_GATE'));
281
+ pipeline.artifacts.push(makeArtifact('audit_report', 'AUDIT'));
282
+
283
+ // Without check results — still should fail
284
+ const result1 = engine.evaluateGate('PRODUCTION_GATE', pipeline);
285
+ expect(result1.pass).toBe(false);
286
+
287
+ // Add check results
288
+ pipeline.gateChecks['PRODUCTION_GATE'] = [
289
+ { check_type: 'build', status: 'pass', command: 'npm run build', exit_code: 0, duration_ms: 1000, timestamp: '' },
290
+ { check_type: 'test', status: 'pass', command: 'npm test', exit_code: 0, duration_ms: 2000, timestamp: '' },
291
+ { check_type: 'lint', status: 'pass', command: 'npm run lint', exit_code: 0, duration_ms: 500, timestamp: '' },
292
+ { check_type: 'typecheck', status: 'pass', command: 'tsc', exit_code: 0, duration_ms: 300, timestamp: '' },
293
+ ];
294
+
295
+ const result2 = engine.evaluateGate('PRODUCTION_GATE', pipeline);
296
+ expect(result2.pass).toBe(true);
297
+ });
298
+ });
299
+
300
+ describe('callbacks', () => {
301
+ it('should track phase transitions in log', () => {
302
+ const outcomes = new Map<PipelinePhase, boolean>([
303
+ ['INTAKE', true],
304
+ ['CONSENSUS_MASTER_PLAN', true],
305
+ ]);
306
+
307
+ const result = simulateOrchestratorLoop('INTAKE', pipeline, outcomes);
308
+ expect(result.phaseLog.length).toBeGreaterThan(1);
309
+ expect(result.phaseLog[0]).toBe('INTAKE');
310
+ });
311
+ });
312
+
313
+ // ─── v1.1 Gap Fix Tests ────────────────────────────────
314
+
315
+ describe('v1.1: CR routing (Gap #1)', () => {
316
+ it('should route to consensus phase when pending CRs exist after REVIEW', () => {
317
+ // Simulate: REVIEW passes but has a pending CR targeting CONSENSUS_MASTER_PLAN
318
+ pipeline.pendingChangeRequests = [
319
+ { cr_id: 'CR-001', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
320
+ ];
321
+
322
+ const engine = createGateEngine();
323
+ let phase: PipelinePhase = 'REVIEW';
324
+ const log: PipelinePhase[] = [phase];
325
+
326
+ // REVIEW gate passes
327
+ const gateResult: GateResult = {
328
+ phase: 'REVIEW', pass: true, blockers: [],
329
+ missingArtifacts: [], failedChecks: [], timestamp: '',
330
+ };
331
+
332
+ // Check for pending CRs (same logic as orchestrator)
333
+ const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
334
+ if (crCheckPhases.has(phase)) {
335
+ const pending = pipeline.pendingChangeRequests;
336
+ const nextCR = pending?.find((cr) => cr.status === 'proposed');
337
+ if (nextCR) {
338
+ nextCR.status = 'approved';
339
+ phase = nextCR.target_phase;
340
+ log.push(phase);
341
+ }
342
+ }
343
+
344
+ expect(phase).toBe('CONSENSUS_MASTER_PLAN');
345
+ expect(pipeline.pendingChangeRequests![0].status).toBe('approved');
346
+ expect(log).toEqual(['REVIEW', 'CONSENSUS_MASTER_PLAN']);
347
+ });
348
+
349
+ it('should route to consensus phase when pending CRs exist after AUDIT', () => {
350
+ pipeline.pendingChangeRequests = [
351
+ { cr_id: 'CR-002', change_type: 'architecture', target_phase: 'CONSENSUS_ARCHITECTURE', status: 'proposed' },
352
+ ];
353
+
354
+ let phase: PipelinePhase = 'AUDIT';
355
+ const log: PipelinePhase[] = [phase];
356
+
357
+ const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
358
+ if (crCheckPhases.has(phase)) {
359
+ const nextCR = pipeline.pendingChangeRequests?.find((cr) => cr.status === 'proposed');
360
+ if (nextCR) {
361
+ nextCR.status = 'approved';
362
+ phase = nextCR.target_phase;
363
+ log.push(phase);
364
+ }
365
+ }
366
+
367
+ expect(phase).toBe('CONSENSUS_ARCHITECTURE');
368
+ expect(pipeline.pendingChangeRequests![0].status).toBe('approved');
369
+ });
370
+
371
+ it('should not re-route already approved CRs', () => {
372
+ pipeline.pendingChangeRequests = [
373
+ { cr_id: 'CR-003', change_type: 'config', target_phase: 'QA_VALIDATION', status: 'approved' },
374
+ ];
375
+
376
+ let phase: PipelinePhase = 'REVIEW';
377
+ const nextCR = pipeline.pendingChangeRequests?.find((cr) => cr.status === 'proposed');
378
+ expect(nextCR).toBeUndefined();
379
+ // Phase should continue normally (no routing)
380
+ });
381
+
382
+ it('should process multiple CRs one at a time', () => {
383
+ pipeline.pendingChangeRequests = [
384
+ { cr_id: 'CR-A', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
385
+ { cr_id: 'CR-B', change_type: 'architecture', target_phase: 'CONSENSUS_ARCHITECTURE', status: 'proposed' },
386
+ ];
387
+
388
+ // First routing: picks CR-A
389
+ const firstCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
390
+ expect(firstCR?.cr_id).toBe('CR-A');
391
+ firstCR!.status = 'approved';
392
+
393
+ // Second routing: picks CR-B
394
+ const secondCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
395
+ expect(secondCR?.cr_id).toBe('CR-B');
396
+ secondCR!.status = 'approved';
397
+
398
+ // No more pending CRs
399
+ const thirdCR = pipeline.pendingChangeRequests.find((cr) => cr.status === 'proposed');
400
+ expect(thirdCR).toBeUndefined();
401
+ });
402
+
403
+ it('should not route CRs after non-review/audit phases', () => {
404
+ pipeline.pendingChangeRequests = [
405
+ { cr_id: 'CR-X', change_type: 'scope', target_phase: 'CONSENSUS_MASTER_PLAN', status: 'proposed' },
406
+ ];
407
+
408
+ const crCheckPhases = new Set<PipelinePhase>(['REVIEW', 'AUDIT']);
409
+ // IMPLEMENTATION is not a CR-check phase
410
+ expect(crCheckPhases.has('IMPLEMENTATION')).toBe(false);
411
+ expect(crCheckPhases.has('QA_VALIDATION')).toBe(false);
412
+ expect(crCheckPhases.has('PRODUCTION_GATE')).toBe(false);
413
+ });
414
+ });
415
+
416
+ describe('v1.1: Constitution verification (Gap #2)', () => {
417
+ it('should block gate progression when constitution is invalid', () => {
418
+ const engine = createGateEngine();
419
+
420
+ // Provide all required artifacts for INTAKE
421
+ pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
422
+ pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
423
+ pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
424
+
425
+ // Without constitution check — passes
426
+ const passResult = engine.evaluateGate('INTAKE', pipeline);
427
+ expect(passResult.pass).toBe(true);
428
+
429
+ // With invalid constitution — fails
430
+ const failResult = engine.evaluateGate('INTAKE', pipeline, {
431
+ constitutionValid: false,
432
+ constitutionReason: 'Hash mismatch: constitution modified after INTAKE',
433
+ });
434
+ expect(failResult.pass).toBe(false);
435
+ expect(failResult.blockers).toContain('Hash mismatch: constitution modified after INTAKE');
436
+ });
437
+
438
+ it('should pass gate when constitution is valid', () => {
439
+ const engine = createGateEngine();
440
+ pipeline.artifacts.push(makeArtifact('master_plan', 'INTAKE'));
441
+ pipeline.artifacts.push(makeArtifact('repo_snapshot', 'INTAKE'));
442
+ pipeline.artifacts.push(makeArtifact('constitution', 'INTAKE'));
443
+
444
+ const result = engine.evaluateGate('INTAKE', pipeline, {
445
+ constitutionValid: true,
446
+ });
447
+ expect(result.pass).toBe(true);
448
+ });
449
+
450
+ it('should apply constitution check to non-INTAKE phases too', () => {
451
+ const engine = createGateEngine();
452
+
453
+ // Setup for ARCHITECTURE phase
454
+ pipeline.artifacts.push(makeArtifact('architecture', 'ARCHITECTURE'));
455
+
456
+ const result = engine.evaluateGate('ARCHITECTURE', pipeline, {
457
+ constitutionValid: false,
458
+ constitutionReason: 'Constitution tampered',
459
+ });
460
+ expect(result.pass).toBe(false);
461
+ expect(result.blockers.some((b: string) => b.includes('Constitution'))).toBe(true);
462
+ });
463
+ });
464
+
465
+ describe('v1.1: gateResult merge (Gap #3)', () => {
466
+ it('should preserve consensus score when merging gate result', () => {
467
+ // Simulate: consensus phase stored a score in gateResults
468
+ pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = {
469
+ phase: 'CONSENSUS_MASTER_PLAN',
470
+ pass: true,
471
+ score: 0.85,
472
+ consensusScore: 0.92,
473
+ blockers: [],
474
+ missingArtifacts: [],
475
+ failedChecks: [],
476
+ timestamp: new Date().toISOString(),
477
+ };
478
+
479
+ // Gate engine produces a new result (without score)
480
+ const newGateResult: GateResult = {
481
+ phase: 'CONSENSUS_MASTER_PLAN',
482
+ pass: true,
483
+ blockers: [],
484
+ missingArtifacts: [],
485
+ failedChecks: [],
486
+ timestamp: new Date().toISOString(),
487
+ };
488
+
489
+ // Merge logic (same as orchestrator's mergeGateResult)
490
+ const existing = pipeline.gateResults['CONSENSUS_MASTER_PLAN'];
491
+ if (existing?.score !== undefined || existing?.consensusScore !== undefined) {
492
+ pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = {
493
+ ...newGateResult,
494
+ score: existing.score ?? newGateResult.score,
495
+ consensusScore: existing.consensusScore ?? newGateResult.consensusScore,
496
+ };
497
+ } else {
498
+ pipeline.gateResults['CONSENSUS_MASTER_PLAN'] = newGateResult;
499
+ }
500
+
501
+ // Scores should be preserved
502
+ const merged = pipeline.gateResults['CONSENSUS_MASTER_PLAN'];
503
+ expect(merged.score).toBe(0.85);
504
+ expect(merged.consensusScore).toBe(0.92);
505
+ // But pass/blockers come from the new gate result
506
+ expect(merged.pass).toBe(true);
507
+ expect(merged.blockers).toEqual([]);
508
+ });
509
+
510
+ it('should overwrite when no prior scores exist', () => {
511
+ const newGateResult: GateResult = {
512
+ phase: 'INTAKE',
513
+ pass: true,
514
+ blockers: [],
515
+ missingArtifacts: [],
516
+ failedChecks: [],
517
+ timestamp: new Date().toISOString(),
518
+ };
519
+
520
+ // No existing entry
521
+ const existing = pipeline.gateResults['INTAKE'];
522
+ expect(existing).toBeUndefined();
523
+
524
+ // Merge should just set the new result
525
+ pipeline.gateResults['INTAKE'] = newGateResult;
526
+ expect(pipeline.gateResults['INTAKE']).toBe(newGateResult);
527
+ });
528
+
529
+ it('should preserve score even when gate fails', () => {
530
+ pipeline.gateResults['CONSENSUS_ARCHITECTURE'] = {
531
+ phase: 'CONSENSUS_ARCHITECTURE',
532
+ pass: true,
533
+ score: 0.7,
534
+ blockers: [],
535
+ missingArtifacts: [],
536
+ failedChecks: [],
537
+ timestamp: '',
538
+ };
539
+
540
+ const failingGateResult: GateResult = {
541
+ phase: 'CONSENSUS_ARCHITECTURE',
542
+ pass: false,
543
+ blockers: ['Missing required artifact'],
544
+ missingArtifacts: [],
545
+ failedChecks: [],
546
+ timestamp: '',
547
+ };
548
+
549
+ const existing = pipeline.gateResults['CONSENSUS_ARCHITECTURE'];
550
+ if (existing?.score !== undefined || existing?.consensusScore !== undefined) {
551
+ pipeline.gateResults['CONSENSUS_ARCHITECTURE'] = {
552
+ ...failingGateResult,
553
+ score: existing.score ?? failingGateResult.score,
554
+ consensusScore: existing.consensusScore ?? failingGateResult.consensusScore,
555
+ };
556
+ }
557
+
558
+ const merged = pipeline.gateResults['CONSENSUS_ARCHITECTURE'];
559
+ expect(merged.pass).toBe(false); // gate engine says fail
560
+ expect(merged.score).toBe(0.7); // consensus score preserved
561
+ expect(merged.blockers).toEqual(['Missing required artifact']);
562
+ });
563
+ });
564
+
565
+ describe('v1.1: RCA rewind (Gap #4)', () => {
566
+ it('should return to failed phase after recovery when no RCA rewind target', () => {
567
+ // Manual simulation: phase fails -> recovery -> back to failed phase
568
+ let phase: PipelinePhase = 'QA_VALIDATION';
569
+ let failedPhase: PipelinePhase | null = null;
570
+
571
+ // QA fails
572
+ failedPhase = phase;
573
+ phase = 'RECOVERY_LOOP';
574
+ pipeline.recoveryCount++;
575
+
576
+ // Recovery succeeds, no RCA rewind target
577
+ // Without RCA, should return to failedPhase
578
+ phase = failedPhase ?? 'QA_VALIDATION';
579
+ failedPhase = null;
580
+
581
+ expect(phase).toBe('QA_VALIDATION');
582
+ });
583
+
584
+ it('should use RCA rewind target when available', () => {
585
+ // Simulate: RCA says rewind to ARCHITECTURE
586
+ let phase: PipelinePhase = 'QA_VALIDATION';
587
+ let failedPhase: PipelinePhase | null = 'QA_VALIDATION';
588
+
589
+ phase = 'RECOVERY_LOOP';
590
+ pipeline.recoveryCount++;
591
+
592
+ // Recovery succeeds, RCA has rewind target
593
+ const rca = { requires_phase_rewind_to: 'ARCHITECTURE' as PipelinePhase };
594
+ if (rca.requires_phase_rewind_to) {
595
+ phase = rca.requires_phase_rewind_to;
596
+ } else {
597
+ phase = failedPhase ?? 'QA_VALIDATION';
598
+ }
599
+ failedPhase = null;
600
+
601
+ expect(phase).toBe('ARCHITECTURE');
602
+ });
603
+
604
+ it('should default to QA_VALIDATION when no failed phase tracked', () => {
605
+ let phase: PipelinePhase = 'RECOVERY_LOOP';
606
+ let failedPhase: PipelinePhase | null = null;
607
+
608
+ // Recovery succeeds, no RCA, no failedPhase
609
+ phase = failedPhase ?? 'QA_VALIDATION';
610
+
611
+ expect(phase).toBe('QA_VALIDATION');
612
+ });
613
+ });
614
+ });